Well, after spending a bunch of time struggling with the methods that were suggested, I decided to just roll my own solution using a pure documents approach.
This method does not do anything with rich text fields and uploads the files directly to the documents object. I'm not really an expert on Apex or Visualforce so I imagine there are some things I did here that aren't quite as elegant as they can be, but I am pretty pleased with the results and wanted to share this with you guys to gather feedback on how I can improve it and how it stacks up against the methods you guys suggested.
Here is my Apex class:
public class ProductPhotosExtension {
private final Product2 prod;
private String imageURL;
private String errorURL = '/apex/ProductPhotoError';
public transient Blob fileBody {
get; set;
}
public String fileName {
get; set;
}
public ProductPhotosExtension(ApexPages.StandardController stdController) {
stdController.addFields(new String[] {
'Id',
'Name',
'Product_Image_ID__c',
'Product_Image__c'
});
this.prod = (Product2)stdController.getRecord();
}
private PageReference showError(String errorTitle, String errorText) {
errorTitle = EncodingUtil.urlEncode(errorTitle, 'UTF-8');
errorText = EncodingUtil.urlEncode(errorText, 'UTF-8');
errorURL = errorURL + '?errorTitle=' + errorTitle;
errorURL = errorURL + '&errorText=' + errorText;
errorURL = errorURL + '&id=' + prod.Id;
PageReference errPage = new PageReference(errorURL);
return errPage;
}
public PageReference deletePhoto() {
try {
Document document = [SELECT Id FROM Document WHERE Id = :prod.Product_Image_ID__c];
prod.Product_Image_ID__c = null;
prod.Product_Image__c = null;
update prod;
delete document;
} catch (Exception e) {
String errorTitle = 'Error deleting photo';
String errorText = 'The photo may have already been deleted.';
return showError(errorTitle, errorText);
}
return null;
}
public PageReference upload() {
Product2 refreshed_prod = [SELECT Id, Product_Image_ID__c FROM Product2 WHERE Id = :prod.Id];
if (refreshed_prod.Product_Image_ID__c != null) {
String errorTitle = 'Error uploading photo';
String errorText = 'This product already has a photo.';
return showError(errorTitle, errorText);
}
Document document = new Document();
document.Body = fileBody;
document.Name = fileName;
document.FolderId = '00l33000001ko9v';
try {
imageURL = '/servlet/servlet.FileDownload?file=';
insert document;
imageURL = imageURL + document.id;
prod.Product_Image_ID__c = document.id;
prod.Product_Image__c = imageURL;
update prod;
} catch (Exception e) {
String errorTitle = 'Error uploading photo';
String errorText = 'Please make sure that the image file is not over 5MB' +
' and that you have enough space in your Documents';
return showError(errorTitle, errorText);
}
return null;
}
public Blob getFileBody() {
return fileBody;
}
public String getFileName() {
return fileName;
}
public String getProductImage() {
return prod.Product_Image__c;
}
public String getProductImageID() {
return prod.Product_Image_ID__c;
}
}
Here is the product uploader widget I embedded in my Product page layout (UploadProductPhoto.vfp):
<apex:page standardController="Product2" extensions="ProductPhotosExtension" sidebar="false" showheader="false">
<apex:stylesheet value="{!$Resource.photouploaderstyles}"/>
<apex:form id="photoUploaderForm" styleClass="photoUploaderForm">
<apex:pageBlock mode="maindetail" id="sectionsContainer">
<apex:pageBlockSection columns="1" id="input">
<apex:pageBlockSectionItem id="inputSection" rendered="{!ISBLANK(ProductImageID)}">
<apex:outputLabel value="Product Image" for="fileImageId"/>
<apex:inputFile id="fileImageId" value="{!fileBody}" styleClass="inputFile"
filename="{!fileName}" onchange="uploadFile(event);"/>
</apex:pageBlockSectionItem>
<div id="waitMessage" class="waitMessage">
Uploading file... Please wait.
</div>
</apex:pageBlockSection>
<apex:pageBlockSection id="preview" columns="1" rendered="{!NOT(ISBLANK(ProductImageID))}">
<apex:pageBlockSectionItem id="previewSection">
<apex:outputLabel value="Product Image" for="previewImageId"/>
<a href="{!ProductImage}" target="blank">
<img id="imagePreview" src="{!ProductImage}" class="previewImage"/>
</a>
<a href="javascript:deleteFile();" class="deleteButton">X</a>
</apex:pageBlockSectionItem>
</apex:pageBlockSection>
</apex:pageBlock>
<apex:actionFunction name="callUploadMethod" action="{!upload}"/>
<apex:actionFunction name="callDeleteMethod" action="{!deletePhoto}"/>
</apex:form>
<script>
var input_divs = document.getElementsByClassName('inputFile');
var input_div = input_divs[0];
var wait_div = document.getElementById('waitMessage');
function getOffset(el) {
el = el.getBoundingClientRect();
return {
left: el.left + window.scrollX,
top: el.top + window.scrollY
}
}
function deleteFile(event) {
callDeleteMethod();
}
function uploadFile(event) {
var file = event.target.files[0];
var reader = new FileReader();
reader.addEventListener("load", function () {
callUploadMethod();
input_div.setAttribute('type', 'hidden');
input_rect = getOffset(input_div);
wait_div.style.left = input_rect.left + 'px';
wait_div.style.top = input_rect.top + 'px';
wait_div.style.display = 'block';
}, false);
reader.readAsDataURL(file);
}
</script>
</apex:page>
Here is the stylesheet for the photo widget (photouploaderstyles.css):
.photoUploaderForm {
width: 94%;
margin-left: 18%;
position: fixed;
overflow: hidden;
}
@media (max-width: 563px) {
.photoUploaderForm {
width: 155%;
margin-left: 5%;
}
}
.inputFile {
width: 80%;
}
@media (max-width: 563px) {
.inputFile {
width: 45%;
}
}
.waitMessage {
position: absolute;
text-align: center;
padding: 5px 2% 5px 2%;
background: #ffffff;
border: 1px solid;
border-color: #000000;
display: none;
width: 50%;
}
.previewImage {
border: 1px dashed;
border-color: #cccccc;
height: 50px;
padding: 2px;
}
.deleteButton {
top: 10px;
font-size: 0.6em;
font-weight: 800;
position: absolute;
background: #ff8989;
margin: -1px 0px 0px 2px;
padding: 1px 5px 1px 5px;
text-decoration: none;
border: 1px solid;
border-color: #ff0000;
}
.deleteButton:link, .deleteButton:visited {
color: #ffffff;
}
.deleteButton:hover {
background: #ff0000;
text-decoration: none;
}
.deleteButton:active {
color: #ff0000;
background: #ffa0a0;
}
I also made a neat little error widget (ProductPhotoError) for handling errors, since the standard apex error messages look hideous in small-sized embedded VF pages:
<apex:page sidebar="false" showheader="false">
<apex:stylesheet value="{!$Resource.photouploadererrorstyles}"/>
<div class="errorMsgBox">
<div class="errorMsgIcon">
<svg viewBox="0 0 30 30" xmlns="http://www.w3.org/2000/svg">
<circle cx="15" cy="15" r="14px" fill="#a50000"/>
<text x="9" y="21" fill="#ffffff" font-size="2em">x</text>
</svg>
</div>
<div class="errorMsgTitleBar">
<p class="errorMsgTitle">
{!$CurrentPage.parameters.errorTitle}
</p>
</div>
<p class="errorMsgText">
{!$CurrentPage.parameters.errorText}
</p>
<a href="/apex/UploadProductPhoto?id={!$CurrentPage.parameters.Id}" class="errorOKButton">OK</a>
</div>
</apex:page>
And here is the style sheet for the error page (photouploadererrorstyles.css):
.errorMsgBox {
margin: 0px 4% 0px 4%;
border: 1px solid;
border-color: #000000;
padding: 2px;
font-size: 1em;
word-wrap: break-word;
position: relative;
display: block;
}
.errorMsgIcon {
display: block;
height: 30px;
width: 30px;
position: absolute;
left: 5px;
top: 5px;
}
.errorMsgTitleBar {
text-align: center;
margin: 0px;
padding: 0px;
}
.errorMsgTitle {
display: inline-block;
margin: 0px 0px 2px 0px;
padding: 0px;
font-weight: bold;
}
.errorMsgText {
margin: 0px 0px 0px 8%;
padding: 0px 5px 2px 5px;
text-align: center;
}
.errorOKButton {
display: block;
margin: 0px;
padding: 0px;
text-align: center;
}
What do you guys think?
P.S. the widget is embedded in the first column in a 2-column section on the page layout with a width value of "100%" and a height of "75px"