Skip to content

Commit cab2f77

Browse files
committed
feat: add upload from computer button in image ribbon gallery
1 parent 0970ac4 commit cab2f77

File tree

2 files changed

+179
-52
lines changed

2 files changed

+179
-52
lines changed

src/LiveDevelopment/BrowserScripts/RemoteFunctions.js

Lines changed: 89 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2099,6 +2099,24 @@ function RemoteFunctions(config = {}) {
20992099
cursor: pointer !important;
21002100
}
21012101
2102+
.phoenix-select-image-btn {
2103+
background: rgba(255,255,255,0.1) !important;
2104+
border: 1px solid rgba(255,255,255,0.2) !important;
2105+
color: #e8eaf0 !important;
2106+
padding: 6px 12px !important;
2107+
border-radius: 6px !important;
2108+
font-size: 12px !important;
2109+
cursor: pointer !important;
2110+
margin-left: 8px !important;
2111+
white-space: nowrap !important;
2112+
transition: all 0.2s ease !important;
2113+
}
2114+
2115+
.phoenix-select-image-btn:hover {
2116+
background: rgba(255,255,255,0.2) !important;
2117+
border-color: rgba(255,255,255,0.3) !important;
2118+
}
2119+
21022120
.phoenix-ribbon-close {
21032121
background: rgba(0,0,0,0.5) !important;
21042122
border: none !important;
@@ -2192,6 +2210,8 @@ function RemoteFunctions(config = {}) {
21922210
<div class="phoenix-ribbon-search">
21932211
<input type="text" placeholder="Search images..." />
21942212
<button class="phoenix-ribbon-search-btn">Search</button>
2213+
<button class="phoenix-select-image-btn">📁 Select from Computer</button>
2214+
<input type="file" class="phoenix-file-input" accept="image/*" style="display: none;">
21952215
</div>
21962216
<button class="phoenix-ribbon-close">✕</button>
21972217
</div>
@@ -2380,11 +2400,14 @@ function RemoteFunctions(config = {}) {
23802400
},
23812401

23822402
_attachEventHandlers: function() {
2403+
const ribbonContainer = this._shadow.querySelector('.phoenix-image-ribbon');
23832404
const searchInput = this._shadow.querySelector('.phoenix-ribbon-search input');
23842405
const searchButton = this._shadow.querySelector('.phoenix-ribbon-search-btn');
23852406
const closeButton = this._shadow.querySelector('.phoenix-ribbon-close');
23862407
const navLeft = this._shadow.querySelector('.phoenix-ribbon-nav.left');
23872408
const navRight = this._shadow.querySelector('.phoenix-ribbon-nav.right');
2409+
const selectImageBtn = this._shadow.querySelector('.phoenix-select-image-btn');
2410+
const fileInput = this._shadow.querySelector('.phoenix-file-input');
23882411

23892412
if (searchInput && searchButton) {
23902413
const performSearch = (e) => {
@@ -2411,6 +2434,22 @@ function RemoteFunctions(config = {}) {
24112434
});
24122435
}
24132436

2437+
if (selectImageBtn && fileInput) {
2438+
selectImageBtn.addEventListener('click', (e) => {
2439+
e.stopPropagation();
2440+
fileInput.click();
2441+
});
2442+
2443+
fileInput.addEventListener('change', (e) => {
2444+
e.stopPropagation();
2445+
const file = e.target.files[0];
2446+
if (file) {
2447+
this._handleLocalImageSelection(file);
2448+
fileInput.value = '';
2449+
}
2450+
});
2451+
}
2452+
24142453
if (closeButton) {
24152454
closeButton.addEventListener('click', (e) => {
24162455
e.stopPropagation();
@@ -2433,7 +2472,6 @@ function RemoteFunctions(config = {}) {
24332472
}
24342473

24352474
// Prevent clicks anywhere inside the ribbon from bubbling up
2436-
const ribbonContainer = this._shadow.querySelector('.phoenix-image-ribbon');
24372475
if (ribbonContainer) {
24382476
ribbonContainer.addEventListener('click', (e) => {
24392477
e.stopPropagation();
@@ -2521,7 +2559,7 @@ function RemoteFunctions(config = {}) {
25212559
e.preventDefault();
25222560
const filename = this._generateFilename(image);
25232561
const extnName = ".jpg";
2524-
this._useImage(image.url, filename, extnName);
2562+
this._useImage(image.url, filename, extnName, false);
25252563
});
25262564

25272565
thumbDiv.appendChild(img);
@@ -2569,21 +2607,67 @@ function RemoteFunctions(config = {}) {
25692607
return `${cleanSearchTerm}-by-${cleanPhotographerName}`;
25702608
},
25712609

2572-
_useImage: function(imageUrl, filename, extnName) {
2610+
_useImage: function(imageUrl, filename, extnName, isLocalFile) {
25732611
// send the message to the editor instance to save the image and update the source code
25742612
const tagId = this.element.getAttribute("data-brackets-id");
25752613

2576-
window._Brackets_MessageBroker.send({
2614+
const messageData = {
25772615
livePreviewEditEnabled: true,
25782616
useImage: true,
25792617
imageUrl: imageUrl,
25802618
filename: filename,
25812619
extnName: extnName,
25822620
element: this.element,
25832621
tagId: Number(tagId)
2584-
});
2622+
};
2623+
2624+
// if this is a local file we need some more data before sending it to the editor
2625+
if (isLocalFile) {
2626+
messageData.isLocalFile = true;
2627+
// Convert data URL to binary data array for local files
2628+
const byteCharacters = atob(imageUrl.split(',')[1]);
2629+
const byteNumbers = new Array(byteCharacters.length);
2630+
for (let i = 0; i < byteCharacters.length; i++) {
2631+
byteNumbers[i] = byteCharacters.charCodeAt(i);
2632+
}
2633+
messageData.imageData = byteNumbers;
2634+
}
2635+
2636+
window._Brackets_MessageBroker.send(messageData);
25852637
},
25862638

2639+
_handleLocalImageSelection: function(file) {
2640+
if (!file || !file.type.startsWith('image/')) {
2641+
return;
2642+
}
2643+
2644+
const reader = new FileReader();
2645+
reader.onload = (e) => {
2646+
const imageDataUrl = e.target.result;
2647+
2648+
const originalName = file.name;
2649+
const nameWithoutExt = originalName.substring(0, originalName.lastIndexOf('.')) || originalName;
2650+
const extension = originalName.substring(originalName.lastIndexOf('.')) || '.jpg';
2651+
2652+
// we clean the file name because the file might have some chars which might not be compatible
2653+
const cleanName = nameWithoutExt.toLowerCase().replace(/[^a-z0-9]/g, '-').replace(/-+/g, '-').replace(/^-|-$/g, '');
2654+
const filename = cleanName || 'selected-image';
2655+
2656+
// Use the unified _useImage method with isLocalFile flag
2657+
this._useImage(imageDataUrl, filename, extension, true);
2658+
2659+
// Close the ribbon after successful selection
2660+
this.remove();
2661+
};
2662+
2663+
reader.onerror = (error) => {
2664+
console.error('Something went wrong when reading the image:', error);
2665+
};
2666+
2667+
reader.readAsDataURL(file);
2668+
},
2669+
2670+
25872671
create: function() {
25882672
this.remove(); // remove existing ribbon if already present
25892673

src/LiveDevelopment/LivePreviewEdit.js

Lines changed: 90 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -661,64 +661,107 @@ define(function (require, exports, module) {
661661
}
662662
}
663663

664+
/**
665+
* Helper function to update image src attribute and dismiss ribbon gallery
666+
*
667+
* @param {Number} tagId - the data-brackets-id of the image element
668+
* @param {String} targetPath - the full path where the image was saved
669+
* @param {String} filename - the filename of the saved image
670+
*/
671+
function _updateImageAndDismissRibbon(tagId, targetPath, filename) {
672+
const editor = _getEditorAndValidate(tagId);
673+
if (editor) {
674+
const htmlFilePath = editor.document.file.fullPath;
675+
const relativePath = PathUtils.makePathRelative(targetPath, htmlFilePath);
676+
_updateImageSrcAttribute(tagId, relativePath);
677+
} else {
678+
_updateImageSrcAttribute(tagId, filename);
679+
}
680+
681+
// dismiss the image ribbon gallery
682+
const currLiveDoc = LiveDevMultiBrowser.getCurrentLiveDoc();
683+
if (currLiveDoc && currLiveDoc.protocol && currLiveDoc.protocol.evaluate) {
684+
currLiveDoc.protocol.evaluate("_LD.dismissImageRibbonGallery()");
685+
}
686+
}
687+
688+
/**
689+
* helper function to handle 'upload from computer'
690+
* @param {Object} message - the message object
691+
* @param {String} filename - the file name with which we need to save the image
692+
* @param {Directory} projectRoot - the project root in which the image is to be saved
693+
*/
694+
function _handleUseThisImageLocalFiles(message, filename, projectRoot) {
695+
const { tagId, imageData } = message;
696+
697+
const uint8Array = new Uint8Array(imageData);
698+
const targetPath = projectRoot.fullPath + filename;
699+
700+
window.fs.writeFile(targetPath, window.Filer.Buffer.from(uint8Array),
701+
{ encoding: window.fs.BYTE_ARRAY_ENCODING }, (err) => {
702+
if (err) {
703+
console.error('Failed to save image:', err);
704+
} else {
705+
_updateImageAndDismissRibbon(tagId, targetPath, filename);
706+
}
707+
});
708+
}
709+
710+
/**
711+
* helper function to handle 'use this image' button click on remote images
712+
* @param {Object} message - the message object
713+
* @param {String} filename - the file name with which we need to save the image
714+
* @param {Directory} projectRoot - the project root in which the image is to be saved
715+
*/
716+
function _handleUseThisImageRemote(message, filename, projectRoot) {
717+
const { imageUrl, tagId } = message;
718+
719+
fetch(imageUrl)
720+
.then(response => {
721+
if (!response.ok) {
722+
throw new Error(`HTTP error! status: ${response.status}`);
723+
}
724+
return response.arrayBuffer();
725+
})
726+
.then(arrayBuffer => {
727+
const uint8Array = new Uint8Array(arrayBuffer);
728+
const targetPath = projectRoot.fullPath + filename;
729+
730+
window.fs.writeFile(targetPath, window.Filer.Buffer.from(uint8Array),
731+
{ encoding: window.fs.BYTE_ARRAY_ENCODING }, (err) => {
732+
if (err) {
733+
console.error('Failed to save image:', err);
734+
} else {
735+
_updateImageAndDismissRibbon(tagId, targetPath, filename);
736+
}
737+
});
738+
})
739+
.catch(error => {
740+
console.error('Failed to fetch image:', error);
741+
});
742+
}
743+
664744
/**
665745
* This function is called when 'use this image' button is clicked in the image ribbon gallery
746+
* or user loads an image file from the computer
666747
* this is responsible to download the image in the appropriate place
667-
* and also change the src attribute of the element
748+
* and also change the src attribute of the element (by calling appropriate helper functions)
668749
* @param {Object} message - the message object which stores all the required data for this operation
669750
*/
670751
function _handleUseThisImage(message) {
671-
const { imageUrl, filename, tagId } = message;
752+
const filename = message.filename;
672753
const extnName = message.extnName || "jpg";
673754

674755
const projectRoot = ProjectManager.getProjectRoot();
675-
if (!projectRoot) {
676-
console.error('No project root found');
677-
return;
678-
}
756+
if (!projectRoot) { return; }
679757

680758
getUniqueFilename(projectRoot.fullPath, filename, extnName).then((uniqueFilename) => {
681-
fetch(imageUrl)
682-
.then(response => {
683-
if (!response.ok) {
684-
throw new Error(`HTTP error! status: ${response.status}`);
685-
}
686-
return response.arrayBuffer();
687-
})
688-
.then(arrayBuffer => {
689-
const uint8Array = new Uint8Array(arrayBuffer);
690-
691-
const targetPath = projectRoot.fullPath + uniqueFilename;
692-
window.fs.writeFile(targetPath, window.Filer.Buffer.from(uint8Array),
693-
{ encoding: window.fs.BYTE_ARRAY_ENCODING }, (err) => {
694-
if (err) {
695-
console.error('Failed to save image:', err);
696-
} else {
697-
// once the image is saved, we need to update the source code
698-
// so we get the relative path between the current file and the image file
699-
// and that relative path is written as the src value
700-
const editor = _getEditorAndValidate(tagId);
701-
if (editor) {
702-
const htmlFilePath = editor.document.file.fullPath;
703-
const relativePath = PathUtils.makePathRelative(targetPath, htmlFilePath);
704-
_updateImageSrcAttribute(tagId, relativePath);
705-
} else {
706-
// if editor is not available we directly write the image file name as the src value
707-
_updateImageSrcAttribute(tagId, uniqueFilename);
708-
}
709-
710-
// after successful update we dismiss the image ribbon gallery
711-
// to ensure that the user doesn't work with image ribbon gallery on a stale DOM
712-
const currLiveDoc = LiveDevMultiBrowser.getCurrentLiveDoc();
713-
if (currLiveDoc && currLiveDoc.protocol && currLiveDoc.protocol.evaluate) {
714-
currLiveDoc.protocol.evaluate("_LD.dismissImageRibbonGallery()");
715-
}
716-
}
717-
});
718-
})
719-
.catch(error => {
720-
console.error('Failed to fetch image:', error);
721-
});
759+
// check if the image is loaded from computer or from remote
760+
if (message.isLocalFile && message.imageData) {
761+
_handleUseThisImageLocalFiles(message, uniqueFilename, projectRoot);
762+
} else {
763+
_handleUseThisImageRemote(message, uniqueFilename, projectRoot);
764+
}
722765
}).catch(error => {
723766
console.error('Something went wrong when trying to use this image', error);
724767
});

0 commit comments

Comments
 (0)