Updated to nearly fully functional. Pre-release 1. Still needs some UI changes in the slideshow and admin pages (move the save button & fix the save detection for the internship list). Customer had one more page change request which I need to re-define and handle.
This commit is contained in:
57
imports/ui/dialogs/SelectImageDialog.html
Normal file
57
imports/ui/dialogs/SelectImageDialog.html
Normal file
@@ -0,0 +1,57 @@
|
||||
|
||||
<template name="SelectImageDialog">
|
||||
<div id="selectImageDialog" class="modalContent">
|
||||
<!--<span class="modalClose">×</span>-->
|
||||
<div class="dialog">
|
||||
<!--<span class="modalClose close">×</span>-->
|
||||
<div class="leftControls">
|
||||
<div class="controlContainer" style="visibility: {{#if image}}visible{{else}}hidden{{/if}}">
|
||||
<h1>Original Size</h1>
|
||||
<div>Width: {{originalWidth}}, Height: {{originalHeight}}</div>
|
||||
<h1>Width</h1>
|
||||
<div>
|
||||
<a class="leftArrow widthLess"><i class="fa fa-angle-left" aria-hidden="true"></i></a>
|
||||
<input class="number width" type="number" min="1" value="{{currentWidth}}"/>
|
||||
<a class="rightArrow widthMore"><i class="fa fa-angle-right" aria-hidden="true"></i></a>
|
||||
</div>
|
||||
<h1>Height</h1>
|
||||
<div>
|
||||
<a class="leftArrow heightLess"><i class="fa fa-angle-left" aria-hidden="true"></i></a>
|
||||
<input class="number height" type="number" min="1" value="{{currentHeight}}"/>
|
||||
<a class="rightArrow heightMore"><i class="fa fa-angle-right" aria-hidden="true"></i></a>
|
||||
</div>
|
||||
<h1>Compression Level</h1>
|
||||
<div>
|
||||
<a class="leftArrow compressionLess"><i class="fa fa-angle-left" aria-hidden="true"></i></a>
|
||||
<input class="number compression" type="number" min="0" max="10" value="{{currentCompression}}"/>
|
||||
<a class="rightArrow compressionMore"><i class="fa fa-angle-right" aria-hidden="true"></i></a>
|
||||
</div>
|
||||
<div>
|
||||
<label class="checkContainer">
|
||||
<input class="lossyCompression" type="checkbox" checked="{{#if lossyCompression}}checked{{else}}{{/if}}"/>
|
||||
<i class="fa fa-square-o unchecked"></i>
|
||||
<i class="fa fa-check-square-o checked"></i>
|
||||
Lossy Compression
|
||||
</label>
|
||||
</div>
|
||||
<div>
|
||||
Size Estimate: {{compressedSize}}
|
||||
</div>
|
||||
<div>
|
||||
<label class="checkContainer">
|
||||
<input class="previewCompression" type="checkbox" checked="{{#if previewCompression}}checked{{else}}{{/if}}"/>
|
||||
<i class="fa fa-square-o unchecked"></i>
|
||||
<i class="fa fa-check-square-o checked"></i>
|
||||
Preview Compressed Image
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="canvasContainer">
|
||||
<div class="canvasWrapper"><canvas class="insertImageCanvas empty"></canvas></div>
|
||||
<a class="close"><i class="fa fa-times"></i></a>
|
||||
<a class="select" style="visibility: {{#if image}}visible{{else}}hidden{{/if}}"><i class="fa fa-check"></i></a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
118
imports/ui/dialogs/SelectImageDialog.import.styl
vendored
Normal file
118
imports/ui/dialogs/SelectImageDialog.import.styl
vendored
Normal file
@@ -0,0 +1,118 @@
|
||||
#selectImageDialog
|
||||
width: 80%
|
||||
height: 80%
|
||||
.dialog
|
||||
display: table
|
||||
//min-height: 320px
|
||||
//height: auto
|
||||
overflow: hidden
|
||||
height: 100%
|
||||
width: 100%
|
||||
|
||||
.leftControls
|
||||
display: table-cell
|
||||
vertical-align: top
|
||||
background: #EEE
|
||||
height: 100%
|
||||
min-height: 320px
|
||||
min-width: 120px
|
||||
-webkit-box-shadow: 2px 0 5px -2px rgba(0,0,0,0.65)
|
||||
-moz-box-shadow: 2px 0 5px -2px rgba(0,0,0,0.65)
|
||||
box-shadow: 2px 0 5px -2px rgba(0,0,0,0.65)
|
||||
h1
|
||||
font-size: 12px
|
||||
font-weight: 800
|
||||
font-name: Arial, "Sans Serif"
|
||||
text-decoration: none
|
||||
text-transform: uppercase
|
||||
background-color: #7babab
|
||||
padding-top: 1px
|
||||
white-space: nowrap
|
||||
div
|
||||
white-space: nowrap
|
||||
input::-webkit-outer-spin-button, input::-webkit-inner-spin-button
|
||||
//display: none; <- Crashes Chrome on hover
|
||||
-webkit-appearance: none;
|
||||
margin: 0 //Apparently some margin are still there even though it's hidden
|
||||
label.checkContainer
|
||||
line-height: 14px
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
user-select: none;
|
||||
|
||||
input[type='checkbox'], .checked
|
||||
display: none
|
||||
input[type='checkbox']:checked ~ .checked
|
||||
display: inline-block
|
||||
input[type='checkbox']:checked ~ .unchecked
|
||||
display: none
|
||||
.unchecked
|
||||
margin-right: 2px
|
||||
.number
|
||||
display: inline-block
|
||||
width: 100px
|
||||
user-select: none;
|
||||
.leftArrow, .rightArrow
|
||||
display: inline-block
|
||||
//width: 20px
|
||||
padding: 0 4px
|
||||
margin: 0 5px
|
||||
cursor: pointer
|
||||
text-decoration: none
|
||||
border: 0
|
||||
border-radius: 8px
|
||||
user-select: none;
|
||||
.leftArrow:hover, .rightArrow:hover
|
||||
background-color: white
|
||||
.leftArrow:active, .rightArrow:active
|
||||
background-color: #999
|
||||
|
||||
.canvasContainer
|
||||
display: table-cell
|
||||
vertical-align: top
|
||||
height: auto
|
||||
width: 100%
|
||||
padding-left: 4px
|
||||
.canvasWrapper
|
||||
position: relative
|
||||
width: 100%
|
||||
height: 100%
|
||||
overflow: auto
|
||||
.insertImageCanvas
|
||||
position: absolute
|
||||
left: 0
|
||||
right: 0
|
||||
top: 0
|
||||
bottom: 0
|
||||
display: block
|
||||
border: 1px solid #888
|
||||
cursor: pointer
|
||||
//Setting a max causes the canvas to scale automatically
|
||||
//max-height: 100%
|
||||
//max-width: 100%
|
||||
.insertImageCanvas.empty
|
||||
border: 3px dashed #444
|
||||
border-radius: 10px
|
||||
margin: 20px 20px
|
||||
.close, .select
|
||||
position: absolute
|
||||
top: 6px
|
||||
height: 30px
|
||||
width: 30px
|
||||
background-color: #BBB
|
||||
border: 1px solid #999
|
||||
border-radius: 8px
|
||||
line-height 30px
|
||||
text-align: center
|
||||
cursor: pointer
|
||||
.close
|
||||
right: 20px
|
||||
color: #A31
|
||||
.select
|
||||
right: 56px
|
||||
color: #072
|
||||
.close:hover, .select:hover
|
||||
background-color: #EEE
|
||||
.close:active, .select:active
|
||||
background-color: #999
|
||||
382
imports/ui/dialogs/SelectImageDialog.js
Normal file
382
imports/ui/dialogs/SelectImageDialog.js
Normal file
@@ -0,0 +1,382 @@
|
||||
import './SelectImageDialog.html';
|
||||
|
||||
Template.SelectImageDialog.onCreated(function() {
|
||||
this.originalImage = new ReactiveVar(undefined); //The original image. This is used to regenerate the displayed image after altering the rendering pipeline.
|
||||
this.currentWidth = new ReactiveVar(0); //The current width of the image. Must be > 0 and <= the original image width. The aspect ratio must be maintained (there is no reason to ever allow a change in aspect ratio).
|
||||
this.currentHeight = new ReactiveVar(0); //The current height of the image. Must be > 0 and <= the original image height. The aspect ratio must be maintained (there is no reason to ever allow a change in aspect ratio).
|
||||
this.currentCompression = new ReactiveVar(8); //On a range of 0..10 where 10 is maximum, and zero is none.
|
||||
this.lossyCompression = new ReactiveVar(true); //Boolean value indicating whether lossy compression should be utilized to reduce image sizes.
|
||||
this.compressedSize = new ReactiveVar(0); //The current display image's size once compressed (in base64 units). Note we could start saving the image outside the html (non-embeded), in which case the image size would be reduced by 1/4 roughly.
|
||||
this.previewCompression = new ReactiveVar(false); //Whether the viewed image includes the compression.
|
||||
this.convertedImage = undefined;
|
||||
});
|
||||
|
||||
Template.SelectImageDialog.onRendered(function() {
|
||||
let template = this;
|
||||
//TODO: Setup Jimp to edit the image in the canvas
|
||||
|
||||
let $canvas = template.$('.insertImageCanvas');
|
||||
let canvas = $canvas[0];
|
||||
let context = canvas.getContext('2d');
|
||||
|
||||
canvas.height = 300;
|
||||
canvas.width = 300;
|
||||
|
||||
template.readFile = function(file) {
|
||||
let reader = new FileReader();
|
||||
|
||||
reader.onload = function(e) {
|
||||
let image = document.createElement("img");
|
||||
|
||||
image.addEventListener("load", function() {
|
||||
//canvas.height = image.height;
|
||||
//canvas.width = image.width;
|
||||
//$canvas.css({width: image.width, height: image.height});
|
||||
template.currentHeight.set(image.height);
|
||||
template.currentWidth.set(image.width);
|
||||
//context.clearRect(0, 0, canvas.width, canvas.height);
|
||||
//context.drawImage(image, 0, 0);
|
||||
$canvas.removeClass("empty");
|
||||
template.originalImage.set(image);
|
||||
template.rerender();
|
||||
}, false);
|
||||
|
||||
image.src = e.target.result;
|
||||
//var id = 'blobid' + (new Date()).getTime();
|
||||
//var blobCache = tinymce.activeEditor.editorUpload.blobCache;
|
||||
//var base64 = reader.result.split(',')[1];
|
||||
//var blobInfo = blobCache.create(id, file, base64);
|
||||
//blobCache.add(blobInfo);
|
||||
//
|
||||
//// call the callback and populate the Title field with the file name
|
||||
//cb(blobInfo.blobUri(), { title: file.name });
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
};
|
||||
|
||||
context.font = '10pt Arial, Sans Serif';
|
||||
context.fillStyle = 'red';
|
||||
context.textAlign = 'center';
|
||||
context.fillText("Drag & Drop", canvas.width / 2, canvas.height / 2 - 14);
|
||||
context.fillStyle = '#666';
|
||||
context.fillText("or", canvas.width / 2, canvas.height / 2);
|
||||
context.fillStyle = 'red';
|
||||
context.fillText("Click To Add Image", canvas.width / 2, canvas.height / 2 + 16);
|
||||
|
||||
template.changeWidth = function(delta) {
|
||||
let originalImage = template.originalImage.get();
|
||||
let ratio = originalImage.height / originalImage.width;
|
||||
let width = template.currentWidth.get() + delta;
|
||||
let height = 0;
|
||||
|
||||
if(width < 0) width = 1;
|
||||
else if(width > originalImage.width) width = originalImage.width;
|
||||
|
||||
height = Math.round(ratio * width);
|
||||
template.setSize(width, height <= 0 ? 1 : height);
|
||||
};
|
||||
template.changeHeight = function(delta) {
|
||||
let originalImage = template.originalImage.get();
|
||||
let ratio = originalImage.width / originalImage.height;
|
||||
let height = template.currentHeight.get() + delta;
|
||||
let width;
|
||||
|
||||
if(height < 0) height = 1;
|
||||
else if(height > originalImage.height) height = originalImage.height;
|
||||
|
||||
width = Math.round(ratio * height);
|
||||
template.setSize(width <= 0 ? 1 : width, height);
|
||||
};
|
||||
template.setSize = function(width, height) {
|
||||
template.currentWidth.set(width);
|
||||
template.currentHeight.set(height);
|
||||
template.rerender();
|
||||
};
|
||||
template.rerender = function() {
|
||||
let image = template.originalImage.get();
|
||||
|
||||
//Apply the width/height and other changes to the original image and render to the canvas.
|
||||
canvas.height = image.height;
|
||||
canvas.width = image.width;
|
||||
$canvas.css({width: image.width, height: image.height});
|
||||
context.clearRect(0, 0, canvas.width, canvas.height);
|
||||
context.drawImage(image, 0, 0);
|
||||
|
||||
//Apply filters
|
||||
|
||||
//Resize
|
||||
if(template.currentWidth.get() !== image.width && template.currentHeight.get() !== image.height) {
|
||||
let width = template.currentWidth.get();
|
||||
let height = template.currentHeight.get();
|
||||
|
||||
template.resizeCanvas(canvas, width, height, true);
|
||||
$canvas.css({width: width, height: height});
|
||||
}
|
||||
|
||||
//If the user has requested that the display show the compression then get the compressed canvas contents and re-display them in the canvas such that the compression is shown.
|
||||
if(template.previewCompression.get()) {
|
||||
let image = new Image();
|
||||
|
||||
//Save the converted image such that we don't compress an already compressed image if the user saves the changes.
|
||||
template.convertedImage = getImageFromCanvas();
|
||||
//Use an image object to convert the compressed image into something we can render to the canvas.
|
||||
image.onload = function() {
|
||||
context.drawImage(image, 0, 0);
|
||||
};
|
||||
image.src = template.convertedImage;
|
||||
}
|
||||
else {
|
||||
template.convertedImage = undefined;
|
||||
}
|
||||
|
||||
template.collectStatistics();
|
||||
};
|
||||
function getImageFromCanvas() { //Gets the base64 string containing the image with selected compression and in the desired format.
|
||||
let compression = Math.abs(template.currentCompression.get() -10) / 10;
|
||||
let type = template.lossyCompression.get() ? "image/jpeg" : "image/png";
|
||||
|
||||
return canvas.toDataURL(type, compression);
|
||||
}
|
||||
template.saveImage = function() {
|
||||
let dataURL = template.convertedImage ? template.convertedImage : getImageFromCanvas();
|
||||
|
||||
let data = Template.currentData();
|
||||
|
||||
if(data && data.onApply && typeof data.onApply === 'function') {
|
||||
data.onApply(dataURL);
|
||||
}
|
||||
};
|
||||
template.collectStatistics = function() {
|
||||
let imageData = getImageFromCanvas();
|
||||
|
||||
//Save the compressed base64 size. If we were to allow the image to be saved outside the html (non-embedded) then we should multiply this by 0.75 for an estimate of a binary format.
|
||||
//Convert to kilo bytes and ensure the value is at least 1kb (it would be weird to have zero kb files).
|
||||
template.compressedSize.set(Math.max(1, Math.round(imageData.length / 1000)) + "kb");
|
||||
};
|
||||
template.resizeCanvas = function(canvas, width, height, resizeCanvas) {
|
||||
let width_source = canvas.width;
|
||||
let height_source = canvas.height;
|
||||
width = Math.round(width);
|
||||
height = Math.round(height);
|
||||
|
||||
let ratio_w = width_source / width;
|
||||
let ratio_h = height_source / height;
|
||||
let ratio_w_half = Math.ceil(ratio_w / 2);
|
||||
let ratio_h_half = Math.ceil(ratio_h / 2);
|
||||
|
||||
let ctx = canvas.getContext("2d");
|
||||
let img = ctx.getImageData(0, 0, width_source, height_source);
|
||||
let img2 = ctx.createImageData(width, height);
|
||||
let data = img.data;
|
||||
let data2 = img2.data;
|
||||
|
||||
for(let j = 0; j < height; j++) {
|
||||
for(let i = 0; i < width; i++) {
|
||||
let x2 = (i + j * width) * 4;
|
||||
let weight = 0;
|
||||
let weights = 0;
|
||||
let weights_alpha = 0;
|
||||
let gx_r = 0;
|
||||
let gx_g = 0;
|
||||
let gx_b = 0;
|
||||
let gx_a = 0;
|
||||
let center_y = (j + 0.5) * ratio_h;
|
||||
let yy_start = Math.floor(j * ratio_h);
|
||||
let yy_stop = Math.ceil((j + 1) * ratio_h);
|
||||
|
||||
for (let yy = yy_start; yy < yy_stop; yy++) {
|
||||
let dy = Math.abs(center_y - (yy + 0.5)) / ratio_h_half;
|
||||
let center_x = (i + 0.5) * ratio_w;
|
||||
let w0 = dy * dy; //pre-calc part of w
|
||||
let xx_start = Math.floor(i * ratio_w);
|
||||
let xx_stop = Math.ceil((i + 1) * ratio_w);
|
||||
for (let xx = xx_start; xx < xx_stop; xx++) {
|
||||
let dx = Math.abs(center_x - (xx + 0.5)) / ratio_w_half;
|
||||
let w = Math.sqrt(w0 + dx * dx);
|
||||
if (w >= 1) {
|
||||
//pixel too far
|
||||
continue;
|
||||
}
|
||||
//hermite filter
|
||||
weight = 2 * w * w * w - 3 * w * w + 1;
|
||||
let pos_x = 4 * (xx + yy * width_source);
|
||||
//alpha
|
||||
gx_a += weight * data[pos_x + 3];
|
||||
weights_alpha += weight;
|
||||
//colors
|
||||
if (data[pos_x + 3] < 255)
|
||||
weight = weight * data[pos_x + 3] / 250;
|
||||
gx_r += weight * data[pos_x];
|
||||
gx_g += weight * data[pos_x + 1];
|
||||
gx_b += weight * data[pos_x + 2];
|
||||
weights += weight;
|
||||
}
|
||||
}
|
||||
data2[x2] = gx_r / weights;
|
||||
data2[x2 + 1] = gx_g / weights;
|
||||
data2[x2 + 2] = gx_b / weights;
|
||||
data2[x2 + 3] = gx_a / weights_alpha;
|
||||
}
|
||||
}
|
||||
//clear and resize canvas
|
||||
if(resizeCanvas) {
|
||||
canvas.width = width;
|
||||
canvas.height = height;
|
||||
} else {
|
||||
ctx.clearRect(0, 0, width_source, height_source);
|
||||
}
|
||||
|
||||
//draw
|
||||
ctx.putImageData(img2, 0, 0);
|
||||
};
|
||||
});
|
||||
|
||||
Template.SelectImageDialog.events({
|
||||
'click .close': function(event, template) {
|
||||
let data = Template.currentData();
|
||||
|
||||
if(data && data.onClose && typeof data.onClose === 'function') {
|
||||
data.onClose();
|
||||
}
|
||||
},
|
||||
'mouseup .insertImageCanvas': function(event, template) {
|
||||
let input = document.createElement('input');
|
||||
input.setAttribute('type', 'file');
|
||||
input.setAttribute('accept', 'image/*');
|
||||
|
||||
input.onchange = function() {
|
||||
template.readFile(this.files[0]);
|
||||
};
|
||||
|
||||
input.click();
|
||||
},
|
||||
'click .select': function(event, template) {
|
||||
template.saveImage();
|
||||
},
|
||||
'focusout .width': function(event, template) {
|
||||
let width = parseInt(event.target.value);
|
||||
|
||||
if(width !== template.currentWidth.get()) template.changeWidth(event.target.value - template.currentWidth.get());
|
||||
},
|
||||
'keypress .width': function(event, template) {
|
||||
if(event.which === 13) {
|
||||
let width = parseInt(event.target.value);
|
||||
|
||||
if(width !== template.currentWidth.get()) template.changeWidth(event.target.value - template.currentWidth.get());
|
||||
}
|
||||
},
|
||||
'click .widthLess': function(event, template) {
|
||||
template.changeWidth(-1);
|
||||
},
|
||||
'click .widthMore': function(event, template) {
|
||||
template.changeWidth(1);
|
||||
},
|
||||
'focusout .height': function(event, template) {
|
||||
let height = parseInt(event.target.value);
|
||||
|
||||
if(height !== template.currentHeight.get()) template.changeHeight(height - template.currentHeight.get());
|
||||
},
|
||||
'keypress .height': function(event, template) {
|
||||
if(event.which === 13) {
|
||||
let height = parseInt(event.target.value);
|
||||
|
||||
if(height !== template.currentHeight.get()) template.changeHeight(height - template.currentHeight.get());
|
||||
}
|
||||
},
|
||||
'click .heightLess': function(event, template) {
|
||||
template.changeHeight(-1);
|
||||
},
|
||||
'click .heightMore': function(event, template) {
|
||||
template.changeHeight(1);
|
||||
},
|
||||
'dragover .insertImageCanvas': function(event, template) {
|
||||
event.preventDefault();
|
||||
},
|
||||
'drop .insertImageCanvas': function(event, template) {
|
||||
event.preventDefault();
|
||||
let files = event.originalEvent.dataTransfer.files;
|
||||
|
||||
if(files.length > 0) {
|
||||
let file = files[0];
|
||||
|
||||
if(typeof FileReader !== 'undefined' && file.type.indexOf("image") !== -1) {
|
||||
template.readFile(file);
|
||||
}
|
||||
}
|
||||
},
|
||||
'focusout .compression': function(event, template) {
|
||||
let compression = parseInt(event.target.value);
|
||||
|
||||
if(compression !== template.currentCompression.get()) {
|
||||
template.currentCompression.set(compression >= 0 ? (compression <= 10 ? compression : 10) : 0);
|
||||
template.collectStatistics();
|
||||
}
|
||||
},
|
||||
'keypress .compression': function(event, template) {
|
||||
if(event.which === 13) {
|
||||
let compression = parseInt(event.target.value);
|
||||
|
||||
if(compression !== template.currentCompress.get()) {
|
||||
template.currentCompression.set(compression >= 0 ? (compression <= 10 ? compression : 10) : 0);
|
||||
|
||||
if(template.previewCompression.get()) template.rerender();
|
||||
else template.collectStatistics();
|
||||
}
|
||||
}
|
||||
},
|
||||
'click .compressionLess': function(event, template) {
|
||||
let compression = template.currentCompression.get() - 1;
|
||||
template.currentCompression.set(compression >= 0 ? compression : 0);
|
||||
|
||||
if(template.previewCompression.get()) template.rerender();
|
||||
else template.collectStatistics();
|
||||
},
|
||||
'click .compressionMore': function(event, template) {
|
||||
let compression = template.currentCompression.get() + 1;
|
||||
template.currentCompression.set(compression <= 10 ? compression : 10);
|
||||
|
||||
if(template.previewCompression.get()) template.rerender();
|
||||
else template.collectStatistics();
|
||||
|
||||
},
|
||||
'change .lossyCompression': function(event, template) {
|
||||
template.lossyCompression.set(event.target.checked);
|
||||
template.collectStatistics();
|
||||
},
|
||||
'change .previewCompression': function(event, template) {
|
||||
template.previewCompression.set(event.target.checked);
|
||||
template.rerender();
|
||||
}
|
||||
});
|
||||
|
||||
Template.SelectImageDialog.helpers({
|
||||
image: function() {
|
||||
return Template.instance().originalImage.get();
|
||||
},
|
||||
originalWidth: function() {
|
||||
let image = Template.instance().originalImage.get();
|
||||
return image ? image.width : 0;
|
||||
},
|
||||
originalHeight: function() {
|
||||
let image = Template.instance().originalImage.get();
|
||||
return image ? image.height : 0;
|
||||
},
|
||||
currentWidth: function() {
|
||||
return Template.instance().currentWidth.get();
|
||||
},
|
||||
currentHeight: function() {
|
||||
return Template.instance().currentHeight.get();
|
||||
},
|
||||
currentCompression() {
|
||||
return Template.instance().currentCompression.get();
|
||||
},
|
||||
lossyCompression() {
|
||||
return Template.instance().lossyCompression.get();
|
||||
},
|
||||
compressedSize() {
|
||||
return Template.instance().compressedSize.get();
|
||||
},
|
||||
previewCompression() {
|
||||
return Template.instance().previewCompression.get();
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user