Skip to content

Instantly share code, notes, and snippets.

@trevorhreed
Last active April 6, 2017 19:47
Show Gist options
  • Save trevorhreed/4055bcf7b354f1b2750ddda3aae5cc36 to your computer and use it in GitHub Desktop.
Save trevorhreed/4055bcf7b354f1b2750ddda3aae5cc36 to your computer and use it in GitHub Desktop.
// #region MISC
/*
Usage:
<image-input content-id="{{show.id}}" image-type="Show" ng-model="show.images"></image-input>
<image-input content-id="{{show.id}}" image-type="ListImage" ng-model="show.listImage"></image-input>
*/
ngm.value('ImageTypeInfo', {
'Document': {
type: 'Document',
label: 'Post (1280x720)',
multiple: false,
minWidth: 1280,
minHeight: 720,
fixedRatio: true
},
'Show': {
type: 'Show',
label: 'Show (1280x720)',
multiple: true,
minWidth: 1280,
minHeight: 720,
fixedRatio: true
},
'Episode': {
type: 'Episode',
label: 'Episode (1280x720)',
multiple: true,
minWidth: 1280,
minHeight: 720,
fixedRatio: true
},
'CastMember': {
type: 'CastMember',
label: 'Cast (1280x720)',
multiple: true,
minWidth: 1280,
minHeight: 720,
fixedRatio: true
},
'Banner': {
type: 'Banner',
label: 'Banner (1280x720)',
multiple: false,
minWidth: 1280,
minHeight: 720,
fixedRatio: true
},
'Logo': {
type: 'Logo',
label: 'Logo (???)',
multiple: false
},
'Background': {
type: 'Background',
label: 'Background (???)',
multiple: false
},
'ListImage': {
type: 'ListImage',
label: 'List (1280x720)',
multiple: false,
minWidth: 1280,
minHeight: 720,
fixedRatio: true
},
'SquareImage': {
type: 'SquareImage',
label: 'Square (1400x1400)',
multiple: false,
minWidth: 1400,
minHeight: 1400,
fixedRatio: true
},
'GalleryPicture': {
type: 'GalleryPicture',
label: 'Gallery (min. height 480px)',
multiple: false,
minHeight: 480
}
});
ngm.directive('ngError', function($parse){
return {
restrict: 'A',
compile: ($element, attr)=>{
let fn = $parse(attr['ngError']);
return (scope, element, attr)=>{
element.on('error', function(event) {
scope.$apply(function() {
fn(scope, {$event:event});
});
});
};
}
};
});
// #endregion
// #region API
ngm.service('imageApi', function(mdeApi){
return mdeApi.createApi({
prep(images){
if(!images) return [];
if(!Array.isArray(images)) images = [images];
return images.map((x)=>{
x.state = x.state || 'keep';
x.originalUrl = x.originalUrl || (x.images && x.images[0] || {}).url;
x.thumbUrl = x.thumbUrl || (x.images && x.images[1] || {}).url;
return x;
}).sort((a, b)=>{
if(a.ordinal < b.ordinal) return -1;
if(a.ordinal > b.ordinal) return 1;
return 0;
});
},
get(contentId, imageTypeInfo){
let images = [];
mdeApi.begin();
if(imageTypeInfo.multiple){
mdeApi(`GET /Image/GetAllImagesForContent?contentId=${contentId}&imageType=${imageTypeInfo.type}`)
.then(x => images = x);
}else{
mdeApi(`GET /Image/GetImage?contentId=${contentId}&imageType=${imageTypeInfo.type}`)
.then(x => images = [x]);
}
return mdeApi.end().then(()=>{
return this.prep(images);
})
},
save(images, contentId){
mdeApi.begin();
if(images){
let imagesToUpdate = [];
if(!Array.isArray(images)) images = [images];
images
.forEach((x)=>{
if(x.state === 'add'){
var data = new FormData();
data.append('Image', x.upload.getFile());
data.append('contentId', contentId);
data.append('imageId', x.imageId);
data.append('imageType', x.imageType);
data.append('ordinal', x.ordinal);
mdeApi(`POST /Image/UploadImage`, data, true);
}else if(x.state === 'delete'){
mdeApi(`POST /Image/RemoveImage?contentId=${contentId}&imageType=${x.imageType}&imageId=${x.imageId}`);
}else{
imagesToUpdate.push(x);
}
});
if(imagesToUpdate.length > 1){
mdeApi(`POST /Image/UpdateContentImageOrder`, { images: imagesToUpdate });
}
}
return mdeApi.end();
}
})
})
// #endregion
// #region Input
ngm.directive('imageInput', function(ImageTypeInfo, imageApi){
return {
require: '?ngModel',
scope: { imageType: '@' },
template: html`
<div layout="row" layout-align="start end">
<image-preview images="data.images" image-type-info="data.imageTypeInfo" image-lightbox="data.images"></image-preview>
<image-selector images="data.images" image-type-info="data.imageTypeInfo" on-change="onChanged($event)"></image-selector>
</div>
<em class="md-body-2">{{data.imageTypeInfo.label || 'Unknown Image Type: ' + imageType}}</em>
`,
link: (scope, element, attrs, ngModelCtrl)=>{
let imageTypeInfo = ImageTypeInfo[scope.imageType];
if(!imageTypeInfo) return element.text('Error: Unknown Image Type');
scope.data = { imageTypeInfo };
if(ngModelCtrl){
ngModelCtrl.$render = () => {
scope.data.images = imageApi.prep(ngModelCtrl.$viewValue);
}
}
scope.onChanged = ($event) => {
if(ngModelCtrl){
let images = angular.copy($event.images);
if(!imageTypeInfo.multiple && Array.isArray(images)){
images = images.shift();
}
ngModelCtrl.$setViewValue(images);
ngModelCtrl.$render();
}
}
}
}
});
// #endregion
// #region Preview
ngm.directive('imagePreview', function(){
return {
restrict: 'E',
scope: {
images: '<',
imageTypeInfo: '<'
},
template: html`
<reference-point ng-if="imageTypeInfo.multiple"></reference-point>
<img ng-src="{{image1.thumbUrl || '-1'}}" ng-error="loadDefault($event)" ng-if="images" />
<img ng-src="{{image2.thumbUrl || '-2'}}" ng-error="loadDefault($event)" ng-if="images && imageTypeInfo && imageTypeInfo.multiple" />
`,
link: (scope, element, attrs) => {
scope.$watch('images', (newImages)=>{
if(!newImages){
scope.image1 = null;
scope.image2 = null;
}else{
let images = angular
.copy(newImages)
.filter(i => i.state !== 'delete')
.sort((a, b)=>{
if(a.ordinal < b.ordinal) return -1;
if(a.ordinal > b.ordinal) return 1;
return 0;
});
scope.image1 = images.shift();
scope.image2 = images.shift();
}
});
let height = 200;
scope.$watch('imageTypeInfo', (imageTypeInfo)=>{
if(!imageTypeInfo) return;
let scaleWidth = imageTypeInfo.minWidth || imageTypeInfo.exactWidth;
let scaleHeight = imageTypeInfo.minHeight || imageTypeInfo.exactHeight;
if(scaleWidth && scaleHeight){
let ratio = (scaleWidth / scaleHeight);
height = 200 / ratio;
}
});
scope.loadDefault = ($event) => {
$event.target.src = `http://placehold.it/200x${height}?text=No Image`;
}
}
}
});
// #endregion
// #region Lightbox
ngm.directive('imageLightbox', function($compile, $timeout){
let lightboxTemplate = html`
<image-lightbox>
<img ng-src="{{images[index].originalUrl}}"
ng-click="moveRightIfExists()"
ng-error="loadDefault($event)"
md-swipe-left="moveRight()"
md-swipe-right="moveLeft()" />
<div class="tools">
<md-button class="md-icon-button mdi-chevron-left" ng-click="moveLeft()" ng-if="images.length > 1"></md-button>
<md-button class="md-icon-button mdi-close" ng-click="close()"></md-button>
<md-button class="md-icon-button mdi-chevron-right" ng-click="moveRight()" ng-if="images.length > 1"></md-button>
</div>
</image-lightbox>
`;
return {
restrict: 'A',
link: (scope, element, attrs) => {
let lightboxScope = scope.$new();
lightboxScope.index = 0;
scope.$watch(attrs.imageLightbox, i => lightboxScope.images = i);
let lightbox;
element.on('click', (e) => {
lightbox = $compile(lightboxTemplate)(lightboxScope);
let image = $('img', lightbox);
let changeImage = (direction) => {
image.fadeOut(()=>{
lightboxScope.index += direction;
if(lightboxScope.index < 0){
lightboxScope.index = lightboxScope.images.length - 1;
}else if(lightboxScope.index >= lightboxScope.images.length){
lightboxScope.index = 0;
}
$timeout(x => image.fadeIn());
});
}
lightboxScope.close = x => lightbox.fadeOut(x => lightbox.remove());
lightboxScope.left = x => changeImage(-1);
lightboxScope.moveRight = x => changeImage(-1);
lightboxScope.moveRightIfExists = x => lightboxScope.images.length > 1 && changeImage(1);
lightbox.hide().appendTo(document.body).fadeIn();
});
}
}
});
// #endregion
// #region Selector
ngm.directive('imageSelector', function($mdDialog, $timeout, imageApi){
let dialog = {
fullscreen: true,
autoWrap: false,
template: html`
<md-dialog class="image-selector">
<md-toolbar class="md-toolbar-tools">
<h2><span>{{title}}</span></h2>
</md-toolbar>
<md-dialog-content class="md-dialog-content">
<div relative layout="column">
<mde-loader active="readingImage"></mde-loader>
<em class="md-body-2">{{imageTypeInfo.label}}</em>
<md-button class="md-primary md-raised"
image-browser-trigger="imageTypeInfo"
image-type-info="imageTypeInfo"
on-load="onImageLoaded($event)"
on-error="onImageLoadError($event)"
ng-click="imageLoadError = ''">
Browse Images
</md-button>
<em class="md-body-1" ng-if="imageLoadError">{{imageLoadError}}</em>
</div>
<h2 class="md-title">{{imageTypeInfo.multiple ? 'Images' : 'Image'}}</h2>
<div class="images">
<div ng-repeat="image in images | filter:isNotDeleted | orderBy:'ordinal'"
data-image-id="{{image.imageId}}" class="image" layout="row" layout-align="start start">
<md-icon md-font-icon="mdi-drag" class="md-primary" hide show-gt-sm ng-if="imageTypeInfo.multiple"></md-icon>
<img ng-src="{{image.thumbUrl}}" width="200" class="preview" />
<div class="actions" layout="column" layout-gt-md="row" layout-align="center center" ng-if="imageTypeInfo.multiple">
<md-button class="md-icon-button md-primary mdi-arrow-up" ng-click="up(image)" ng-disabled="$first">
<md-tooltip>Move Up</md-tooltip>
</md-button>
<md-button class="md-icon-button md-primary mdi-arrow-down" ng-click="down(image)" ng-disabled="$last">
<md-tooltip>Move Down</md-tooltip>
</md-button>
<md-button class="md-icon-button md-warn mdi-delete" ng-click="del(image)">
<md-tooltip>Delete</md-tooltip>
</md-button>
</div>
</div>
</div>
</md-dialog-content>
<md-dialog-actions>
<md-button ng-click="cancel()">Cancel</md-button>
<md-button ng-click="ok()" class="md-primary">OK</md-button>
</md-dialog-actions>
</md-dialog>
`,
controller: function($scope, images, imageTypeInfo){
$scope.images = images;
let makeSortable = ()=>{
$timeout(()=>{
$('.images').sortable({
handle: 'md-icon',
cursor: 'move',
stop: ()=>{
let imageIndex = {};
$('.images .image').each((index, el)=>{
let id = $(el).attr('data-image-id');
imageIndex[id] = index;
});
let images = angular.copy($scope.images);
for (let i = 0; i < images.length; i++) {
let id = images[i].imageId;
images[i].ordinal = imageIndex[id];
}
$timeout(x => $scope.images = images);
}
});
});
}
makeSortable();
$scope.imageTypeInfo = imageTypeInfo;
$scope.title = imageTypeInfo.multiple ? 'Select Images' : 'Select Image';
$scope.isNotDeleted = (image) => {
return image.state !== 'delete';
}
$scope.del = (image) => {
if(image.state === 'add') $scope.images.splice($scope.images.indexOf(image), 1);
else image.state = 'delete';
}
$scope.up = function(image) {
if (image.ordinal === 0) return;
image.ordinal--;
$scope.images.forEach(function(item) {
if (item.ordinal === image.ordinal && item.imageId !== image.imageId) {
item.ordinal++;
}
});
}
$scope.down = function(image) {
if (image.ordinal === $scope.images.length - 1) return;
image.ordinal++;
$scope.images.forEach(function(item) {
if (item.ordinal === image.ordinal && item.imageId !== image.imageId) {
item.ordinal--;
}
});
}
$scope.onImageLoadError = ($event) => {
$scope.imageLoadError = 'Unknown Error.';
}
$scope.onImageLoaded = ($event) => {
$scope.newImage = {
imageType: imageTypeInfo.type,
imageId: imageApi.newId(),
state: 'add',
upload: $event.upload,
originalUrl: $event.upload.url,
thumbUrl: $event.upload.url,
ordinal: $scope.images.length
};
if(imageTypeInfo.multiple){
$scope.images.push($scope.newImage);
}else{
$scope.images = [$scope.newImage];
}
$scope.newImage = null;
makeSortable();
}
$scope.cancel = $mdDialog.cancel;
$scope.ok = () => {
$mdDialog.hide($scope.images);
}
}
};
return {
scope: {
images: '<',
imageTypeInfo: '<',
onChange: '&?'
},
template: html`
<md-button class="md-icon-button mdi-upload" ng-click="open()">
<md-tooltip>Upload Image</md-tooltip>
</md-button>
`,
link: (scope, element, attrs)=>{
scope.open = () => {
let dialogInstance = angular.extend({
locals: {
images: angular.copy(scope.images || {}),
imageTypeInfo: scope.imageTypeInfo
}
}, dialog);
$mdDialog
.show(dialogInstance)
.then(images => scope.onChange && scope.onChange({'$event': {images}}));
}
}
}
});
// #endregion
// #region Dropzone
ngm.directive('imageBrowserTrigger', function($timeout, $q, ImageTypeInfo){
return {
restrict: 'A',
scope: {
imageTypeInfo: '<',
onLoad: '&',
onError: '&'
},
link: (scope, element, attrs)=>{
let uploader = document.createElement('INPUT');
uploader.type = 'file';
if(scope.imageTypeInfo.multiple) uploader.setAttribute('multiple', 'multiple');
uploader.addEventListener('change', e => loadFile(uploader.files[0]));
element.on('click', x => uploader.click());
let {exactWidth, exactHeight, minWidth, minHeight, fixedRatio} = scope.imageTypeInfo || {};
let validateImage = (url) => {
let hasRequirements = exactWidth || exactHeight || minWidth || minHeight;
if(!hasRequirements) return $q.when();
return $q((resolve, reject)=>{
let image = new Image();
image.onload = x => resolve(image);
image.src = url;
}).then((image)=>{
if(exactWidth && exactWidth !== image.width) throw `Invalid width: must be ${exactWidth} pixels.`;
if(exactHeight && exactHeight !== image.height) throw `Invalid height: must be ${exactHeight} pixels.`;
if(minWidth && minWidth > image.width) throw `Invalid width: must be at least ${minWidth} pixels.`;
if(minHeight && minHeight > image.height) throw `Invalid height: must be at least ${minHeight} pixels.`;
let imageRatio = (image.width / image.height);
if(fixedRatio && exactWidth && exactHeight && (exactWidth / exactHeight) !== imageRatio) throw `Invalid aspect ratio: must be ${exactWidth}x${exactHeight}.`;
if(fixedRatio && minWidth && minHeight && (minWidth / minHeight) !== imageRatio) throw `Invalid aspect ratio: must be ${minWidth}x${minHeight}.`;
return;
});
}
let loadFile = (file) => {
$timeout(()=>{
scope.loading = $q((resolve, reject)=>{
let reader = new FileReader();
reader.onload = resolve;
reader.readAsDataURL(file);
}).then((e)=>{
let url = e.target.result;
validateImage(url)
.then(()=>{
scope.image = { getFile(){ return file }, url };
scope.onLoad({'$event': { upload: scope.image }});
})
.catch((err)=>{
scope.image = null;
scope.onError({'$event': err});
});
});
});
}
}
}
});
ngm.directive('imageDropzone', function($timeout, $q, ImageTypeInfo){
return {
scope: {
imageTypeInfo: '<',
width: '@',
height: '@',
text: '@',
onLoad: '&',
onError: '&'
},
template: html`
<mde-loader active="loading"></mde-loader>
<inner-text ng-if="!image">{{message}}</inner-text>
<img ng-src="{{image.url}}" ng-if="image" />
`,
link: (scope, element, attrs)=>{
let {exactWidth, exactHeight, minWidth, minHeight, fixedRatio} = scope.imageTypeInfo || {};
scope.message = scope.text;
element.width(scope.width);
element.height(scope.height);
let el = element[0];
let uploader = document.createElement('INPUT');
uploader.type = 'file';
let reset = () => {
scope.image = null;
scope.message = scope.text;
}
let validateImage = (url) => {
let hasRequirements = exactWidth || exactHeight || minWidth || minHeight;
if(!hasRequirements) return $q.when();
return $q((resolve, reject)=>{
let image = new Image();
image.onload = x => resolve(image);
image.src = url;
}).then((image)=>{
if(exactWidth && exactWidth !== image.width) throw `Invalid width: must be ${exactWidth} pixels.`;
if(exactHeight && exactHeight !== image.height) throw `Invalid height: must be ${exactHeight} pixels.`;
if(minWidth && minWidth > image.width) throw `Invalid width: must be at least ${minWidth} pixels.`;
if(minHeight && minHeight > image.height) throw `Invalid height: must be at least ${minHeight} pixels.`;
let imageRatio = (image.width / image.height);
if(fixedRatio && exactWidth && exactHeight && (exactWidth / exactHeight) !== imageRatio) throw `Invalid aspect ratio: must be ${exactWidth}x${exactHeight}.`;
if(fixedRatio && minWidth && minHeight && (minWidth / minHeight) !== imageRatio) throw `Invalid aspect ratio: must be ${minWidth}x${minHeight}.`;
return;
});
}
let loadFile = (file) => {
$timeout(()=>{
scope.loading = $q((resolve, reject)=>{
let reader = new FileReader();
reader.onload = resolve;
reader.readAsDataURL(file);
}).then((e)=>{
let url = e.target.result;
validateImage(url)
.then(()=>{
scope.image = { file, url };
scope.onLoad({'$event': {
upload: scope.image,
reset
}});
})
.catch((err)=>{
scope.image = null;
scope.message = err;
scope.onError({'$event': err});
});
});
});
}
uploader.addEventListener('change', e => loadFile(uploader.files[0]));
el.addEventListener('click', e => uploader.click());
el.addEventListener('dragleave', (e) => {
$timeout(x => element.removeClass('dropping'));
});
el.addEventListener('dragover', (e)=>{
e.preventDefault();
let isDraggingImages = e.dataTransfer.types.indexOf('Files') !== -1 && e.dataTransfer.items[0].type.indexOf('image/') === 0;
if(isDraggingImages){
$timeout(()=>{
e.dataTransfer.dropEffect = 'copy';
element.addClass('dropping');
});
}
});
el.addEventListener('drop', (e)=>{
e.preventDefault();
let file = e.dataTransfer.files[0];
$timeout(()=>{
element.removeClass('dropping');
loadFile(file);
});
});
}
}
})
// #endregion
/*
SCSS
image-input{
display:inline-block;
margin:18px 0;
padding-left:12px;
em{
padding-left:6px;
color:rgba(0, 0, 0, .5);
}
}
image-preview{
display:inline-block;
position:relative;
cursor:pointer;
img{
border:solid 1px #aaa;
padding:5px;
background:#fff;
box-shadow:0 0 3px #aaa;
width:200px;
}
reference-point + img{
position:absolute;
top:10px;
left:10px;
}
img + img{
margin:0 10px 10px 0;
}
}
image-lightbox{
position:fixed;
top:0;left:0;right:0;bottom:0;
z-index:100;
background:rgba(0, 0, 0, .9);
display:flex;
flex-direction:column;
align-items:center;
justify-content:center;
img{
padding:5px;
border:solid 1px #aaa;
background:#fff;
box-shadow:0 0 3px #000;
max-width:100%;
max-height:100%;
box-sizing:border-box;
}
.tools{
position:absolute;
left:0;right:0;bottom:0;
display:flex;
flex-direction:row;
justify-content:center;
background:rgba(0, 0, 0, .5);
.md-button.md-icon-button::before{
color:#fff;
}
}
}
md-dialog.image-selector{
em{
color:rgba(0, 0, 0, .5);
}
[image-browser-trigger]{
margin-left:0;
margin-right:0;
}
.images{
.image{
margin-bottom:12px;
img.preview{
border:solid 1px #aaa;
padding:5px;
background:#fff;
box-shadow:0 0 3px #aaa;
}
.actions{
.md-icon-button{
margin:0 !important;
}
}
}
}
}
image-dropzone{
position:relative;
display:block;
border:solid 1px #aaa;
padding:5px;
background:#fff;
box-shadow:0 0 3px #aaa;
text-align:center;
cursor:pointer;
transition: all 300ms ease;
&.dropping{
background:#eee;
}
&::before{
content: '';
display: inline-block;
height:100%;
vertical-align: middle;
margin-right: -0.25em;
}
inner-text{
display:inline-block;
vertical-align:middle;
}
img{
max-width:100%;
max-height:100%;
display:inline-block;
vertical-align:middle;
}
}
*/
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment