(function () { //Angular module that has some generic, useful functionality, mainly directives var mod = angular.module('usefulstuff', []); mod.directive('checkList', function () { return { scope: { list: '=checkList', value: '@' }, link: function (scope, elem, attrs) { var handler = function (setup) { var checked = elem.prop('checked'); var index = 0; if (scope.list) index = scope.list.indexOf(scope.value); if (checked && index === -1) { if (setup) elem.prop('checked', false); else scope.list.push(scope.value); } else if (!checked && index !== -1) { if (setup) elem.prop('checked', true); else scope.list.splice(index, 1); } }; var setupHandler = handler.bind(null, true); var changeHandler = handler.bind(null, false); elem.on('change', function () { scope.$apply(changeHandler); }); scope.$watch('list', setupHandler, true); } }; }); mod.directive('shouldMatch', function () { return { require: 'ngModel', restrict: 'A', scope: { shouldMatch: '=' }, link: function (scope, elem, attrs, ctrl) { scope.$watch(function () { var modelValue = ctrl.$modelValue || ctrl.$$invalidModelValue; return (ctrl.$pristine && angular.isUndefined(modelValue)) || scope.shouldMatch === modelValue; }, function (currentValue) { ctrl.$setValidity('shouldMatch', currentValue); }); } }; }); //directive to validate a password mod.directive('valPassword', function () { return { require: 'ngModel', restrict: 'A', link: function (scope, el, attrs, ctrl) { var uppercaseRegex = /[A-Z]+/; var lowercaseRegex = /[a-z]+/; var integerRegex = /[0-9]+/; //when the model changes scope.$watch(function () { return ctrl.$viewValue; // add errors contextually }, function (currentValue) { if (integerRegex.test(currentValue)) ctrl.$setValidity('integer', true); else ctrl.$setValidity('integer', false); if (lowercaseRegex.test(currentValue)) ctrl.$setValidity('lowercase', true); else ctrl.$setValidity('lowercase', false); if (uppercaseRegex.test(currentValue)) ctrl.$setValidity('uppercase', true); else ctrl.$setValidity('uppercase', false); }); } } }); // directive that adds a confirm modal when clicked mod.directive('ngReallyClick', ['$modal', function($modal) { var modalInstanceCtrl = function ($scope, $modalInstance) { $scope.hasBeenClicked = false; $scope.ok = function () { if ($modalInstance) { $scope.hasBeenClicked = true; $modalInstance.close(); } }; $scope.cancel = function () { if ($modalInstance) { $scope.hasBeenClicked = true; $modalInstance.dismiss('cancel'); } }; }; return { restrict: 'A', scope: { ngReallyClick: "&", //declare a function binding for directive item: "=" //the current item for the directive }, link: function(scope, element, attrs) { element.bind('click', function() { var message = attrs.ngReallyMessage; var title = attrs.ngReallyTitle; var modalHtml = ''; if (title) { var modalHtml = ''; } modalHtml += ''; var modalInstance = $modal.open({ template: modalHtml, controller: modalInstanceCtrl, size: 'sm', windowClass: 'center-modal' }); modalInstance.result.then(function () { scope.ngReallyClick({ item: scope.item }); //call the function though function binding }, function () { //Modal dismissed }); }); } } } ]); //directive to make the element draggable mod.directive('draggable', function () { return function (scope, element) { // this gives us the native JS object var el = element[0]; el.draggable = true; el.addEventListener( 'dragstart', function (e) { e.dataTransfer.effectAllowed = 'move'; e.dataTransfer.setData('Text', this.id); this.classList.add('drag'); return false; }, false ); el.addEventListener( 'dragend', function (e) { this.classList.remove('drag'); return false; }, false ); } }); //directive to make the element droppable mod.directive('droppable', function () { return { scope: { drop: '&' // parent }, link: function (scope, element) { // again we need the native object var el = element[0]; el.addEventListener( 'dragover', function (e) { e.dataTransfer.dropEffect = 'move'; // allows us to drop if (e.preventDefault) e.preventDefault(); this.classList.add('over'); return false; }, false ); el.addEventListener( 'dragenter', function (e) { this.classList.add('over'); return false; }, false ); el.addEventListener( 'dragleave', function (e) { this.classList.remove('over'); return false; }, false ); el.addEventListener( 'drop', function (e) { // Stops some browsers from redirecting. if (e.stopPropagation) e.stopPropagation(); this.classList.remove('over'); var item = document.getElementById(e.dataTransfer.getData('Text')); this.appendChild(item); // call the drop passed drop function scope.$apply('drop()'); return false; }, false ); } } }); mod.directive('autoHeight', function () { return { restrict: 'A', link: function (scope, element, $attrs) { $(window).on('scroll', function () { if (element[0].contentWindow) { var iFrameHeight = element[0].contentWindow.document.getElementById('showcase-view').offsetHeight + 'px'; element.css('height', iFrameHeight); } }); } } }); mod.directive("whenScrolled", function ($window) { return { restrict: 'A', link: function (scope, elem, attrs) { raw = elem[0]; var checkBounds = function (evt) { var rectObject = raw.getBoundingClientRect(); if ($window.innerHeight > rectObject.bottom) { scope.loading = true; scope.$apply(attrs.whenScrolled); } }; angular.element($window).bind('scroll load', checkBounds); } }; }); mod.directive("whenScrolledMobile", function ($window) { return { restrict: 'A', link: function (scope, elem, attrs) { raw = elem[0]; var checkBounds = function (evt) { var rectObject = raw.getBoundingClientRect(); if ($window.innerHeight > rectObject.bottom) { scope.loading = true; scope.$apply(attrs.whenScrolledMobile); } }; angular.element($window).bind('scroll load', checkBounds); } }; }); //mod.directive('whenScrolled', function () { // return { // restrict: 'A', // link: function (scope, elm, attr) { // var itemContainer = $('#items'); // $(window).scroll(function () { // if ($(window).scrollTop() >= $(document).height() - $(window).height() * 1.5) { // scope.$apply(attr.whenScrolled); // } // }); // itemContainer.bind('scroll', function (e) { // if (itemContainer.offset().top > 100) { // scope.$apply(attr.whenScrolled); // } // }); // } // } //}); mod.directive('whenScrolledMobile', function () { return { restrict: 'A', link: function (scope, elm, attr) { $(window).scroll(function () { if ($(window).scrollTop() >= $(document).height() - $(window).height() * 2) scope.$apply(attr.whenScrolledMobile); }); } } }); //only update the model when the element has blurred - e.g. don't update the model linked to a text box until after the user has moused away from it mod.directive('ngChangeOnBlur', function () { return { restrict: 'A', require: 'ngModel', link: function (scope, elm, attrs, ngModelCtrl) { if (attrs.type === 'radio' || attrs.type === 'checkbox') return; var expressionToCall = attrs.ngChangeOnBlur; var oldValue = null; elm.bind('focus', function() { scope.$apply(function() { oldValue = elm.val(); }); }); elm.bind('blur', function () { scope.$apply(function () { var newValue = elm.val(); if (newValue !== oldValue) { scope.$eval(expressionToCall); } //alert('changed ' + oldValue); }); }); } }; }); mod.directive('cloudinaryVideoPreview', ['$sce', function ($sce) { var directive = { restrict: 'E', scope: { publicId: '=', w: '@', h: '@', cloudinaryVideoBaseUrl:'=' }, template: '', link: link }; return directive; function link(scope, element, attrs) { scope.$watch(attrs.cloudinaryVideoBaseUrl, function (newValue, oldValue) { if (newValue) { var baseVideoUrl = scope.cloudinaryVideoBaseUrl + 'w_' + scope.w + ',' + 'c_fill' + '/' + scope.publicId; scope.sources = [ { src: $sce.trustAsResourceUrl(baseVideoUrl + ".webm"), type: "video/webm" }, { src: $sce.trustAsResourceUrl(baseVideoUrl + ".mp4"), type: "video/mp4" }, { src: $sce.trustAsResourceUrl(baseVideoUrl + ".ogv"), type: "video/ogg" } ]; scope.posterUrl = baseVideoUrl + '.jpg'; } }); } }]); mod.directive("vgSrc", function () { return { restrict: "A", link: { pre: function (scope, elem, attr) { var element = elem; var sources; var canPlay; function changeSource() { if (sources) { for (var i = 0, l = sources.length; i < l; i++) { canPlay = element[0].canPlayType(sources[i].type); if (canPlay === "maybe" || canPlay === "probably") { element.attr("src", sources[i].src); element.attr("type", sources[i].type); break; } } } if (canPlay === "") scope.$emit("onVideoError", { type: "Can't play file" }); } scope.$watch(attr.vgSrc, function (newValue, oldValue) { if (!sources || newValue !== oldValue) { sources = newValue; changeSource(); } }); } } } }); //Scroll to href attribute mod.directive('scrollOnClick', function () { return { restrict: 'A', link: function (scope, $elm, attrs) { var idToScroll = attrs.href; $elm.on('click', function () { var $target; if (idToScroll) { $target = $(idToScroll); } else { $target = $elm; } $("body").animate({ scrollTop: $target.offset().top }, "slow"); }); } } }); mod.directive('resize', function ($window) { return { restrict: 'EA', scope: { numberOfItems: '=' }, link: function (scope, element, attrs) { var w = angular.element($window); scope.getWindowDimensions = function () { return { 'w': w.width() }; }; scope.$watch(scope.getWindowDimensions, function (dragContainer, oldValue, lastCss) { // Deduct container padding scope.windowWidth = dragContainer.w - 30; scope.itemWidth = 150; scope.lastWidth = scope.windowWidth - (scope.numberOfItems * scope.itemWidth); scope.$parent.lastCss = { "border": 'none', "border-right": scope.lastWidth + 'px solid transparent', "width": 'auto', "background-size": '150px' }; }, true); w.bind('resize', function () { //scope.$apply(); }); } }; }); //Auto play video onclick (video ifrmae id needs to be in href attribute can be used in conjunction with scrollOnClick mod.directive('playOnClick', function () { return { restrict: 'A', link: function (scope, $elm, attrs) { var video = attrs.href; $elm.on('click', function (e) { var evt = e || window.event; var $target; if (video) { $target = $(video); } else { $target = $elm; } $(video)[0].src += "&autoplay=1"; evt.preventDefault(); }); } } }); // Toggle tag and framework class mod.directive('toggleClass', function () { return { restrict: 'A', link: function (scope, element, attrs) { element.bind('click', function () { $("ul li").removeClass('active text-white'); $("a").removeClass('active text-white'); $(".timeline-header-title").removeClass('active text-white'); $(this).addClass('active text-white'); }); } }; }); // Remove tag and framework class mod.directive('removeClass', function () { return { restrict: 'A', link: function (scope, element, attrs) { element.bind('click', function () { $(".tagged").removeClass(attrs.removeClass); }); } }; }); //directive to determine if the selected file is a video mod.directive('fileIsVideo', function () { return { require: 'ngModel', restrict: 'A', scope: { fileIsVideo: "=" }, link: function (scope, elem, attrs, ctrl) { //use regex to test if the file is the correct type var imageRegex = /(mp4|webm|ogv|3gp|flv|quicktime|mov)/i; scope.$watch(function () { var modelValue = ctrl.$viewValue; if (!modelValue) return true; return imageRegex.test(modelValue); }, function (currentValue) { scope.fileIsVideo = currentValue; }); } }; }); //directive to determine if the selected file is a badge mod.directive('fileIsBadge', function () { return { require: 'ngModel', restrict: 'A', scope: { fileIsBadge: "=" }, link: function (scope, elem, attrs, ctrl) { //use regex to test if the file is the correct type var imageRegex = /png/i; scope.$watch(function () { var modelValue = ctrl.$viewValue; if (!modelValue) return true; var result = imageRegex.test(modelValue); return result; }, function (currentValue) { scope.fileIsBadge = currentValue; }); } }; }); //directive to determine if the selected file is an image mod.directive('fileIsImage', function () { return { require: 'ngModel', restrict: 'A', scope: { fileIsImage: "=" }, link: function (scope, elem, attrs, ctrl) { //use regex to test if the file is the correct type var imageRegex = /(jpg|png|gif|bmp|jpeg)/i; scope.$watch(function () { var modelValue = ctrl.$viewValue; if (!modelValue) return true; return imageRegex.test(modelValue); }, function (currentValue) { scope.fileIsImage = currentValue; }); } }; }); mod.directive("starRating", function () { return { restrict: "EA", templateUrl: modulesSharedResourcesUrl + "Modules/UsefulStuff/Templates/starrating.html?version=160718", scope: { max: "=?", //optional: default is 5 onRatingSelected: "&?", readonly: "=?" }, require: 'ngModel', link: function (scope, elem, attrs, ngModelController) { if (scope.max == undefined) scope.max = 5; function updateStars() { scope.stars = []; for (var i = 0; i < scope.max; i++) { scope.stars.push({ filled: i < ngModelController.$viewValue }); } }; scope.toggle = function (index) { if (scope.readonly == undefined || scope.readonly == false) { updateModel(index + 1); if (scope.onRatingSelected) { scope.onRatingSelected({ rating: index + 1 }); } } }; scope.$watch("max", function (oldVal, newVal) { if (newVal !== undefined) updateStars(); }); // update the model then the view function updateModel(value) { // call $parsers pipeline then update $modelValue ngModelController.$setViewValue(value); // update the local view ngModelController.$render(); } // when model change, update our view (just update the div content) ngModelController.$render = function () { updateStars(); }; } }; }); //a directive that adds max word count validation to a form element mod.directive('maxWordCount', function () { return { // limit usage to argument only restrict: 'A', // require NgModelController, i.e. require a controller of ngModel directive require: 'ngModel', // create linking function and pass in our NgModelController as a 4th argument link: function (scope, element, attr, ctrl) { var initialValidity = attr.minWordCount === '0' || attr.wordValue === 'true'; ctrl.$setValidity('maxWordCount', initialValidity); ctrl.$parsers.unshift(function (viewValue) { if (!viewValue) { ctrl.$setValidity('maxWordCount', true); return ''; } var maxWordCount = attr.maxWordCount; if (maxWordCount == 0) { ctrl.$setValidity('maxWordCount', true); return viewValue; } var text = viewValue; if (attr.stripHtml === 'true') { var htmlRegex = /(<([^>]+)>)/ig; text = viewValue.replace(htmlRegex, ""); } var regex = /\s+/gi; var wordCount = text.trim().replace(regex, ' ').split(' ').length; if (maxWordCount >= wordCount) { ctrl.$setValidity('maxWordCount', true); return viewValue; } else { ctrl.$setValidity('maxWordCount', false); return undefined; } }); } }; }); //a directive to give a dynamic name to a form element - normally form element names are static which don't work in all cases mod.directive('dynamicName', function ($compile, $parse) { return { restrict: 'A', terminal: true, priority: 100000, link: function (scope, elem) { //get the dynamic name var name = $parse(elem.attr('dynamic-name'))(scope); // $interpolate() will support things like 'skill'+skill.id where parse will not //remove the dynamic-name attribute - we don't need it now elem.removeAttr('dynamic-name'); //add a normal name attribute elem.attr('name', name); $compile(elem)(scope); } }; }); mod.directive('minWordCount', function () { return { // limit usage to argument only restrict: 'A', // require NgModelController, i.e. require a controller of ngModel directive require: 'ngModel', // create linking function and pass in our NgModelController as a 4th argument link: function (scope, element, attr, ctrl) { var initialValidity = attr.minWordCount === '0' || attr.wordValue==='true'; ctrl.$setValidity('minWordCount', initialValidity); ctrl.$parsers.unshift(function (viewValue) { if (viewValue === null || viewValue===undefined) { ctrl.$setValidity('minWordCount', true); return ''; } var minWordCount = attr.minWordCount; if (minWordCount == 0) { ctrl.$setValidity('minWordCount', true); return viewValue; } var regex = /\s+/gi; var text = viewValue; if (attr.stripHtml==='true') { var htmlRegex = /(<([^>]+)>)/ig; text = viewValue.replace(htmlRegex, ""); } var wordCount = text.trim().replace(regex, ' ').split(' ').length; if (minWordCount <= wordCount) { ctrl.$setValidity('minWordCount', true); return viewValue; } else { ctrl.$setValidity('minWordCount', false); return undefined; } }); } }; }); mod.directive("dateToIso", function () { var linkFunction = function (scope, element, attrs, ngModelCtrl) { ngModelCtrl.$parsers.push(function (datepickerValue) { return moment(datepickerValue).format("YYYY-MM-DD"); }); }; return { restrict: "A", require: "ngModel", link: linkFunction }; }); // Get Window Width //mod.directive('deviceWidth', function ($window, $rootScope) { // return { // restrict: 'EA', // link: function (scope, element, attrs) { // var deviceWidth = angular.element($window); // // We will also get the height of the creen to enable draggable scrolling on the film strip // scope.deviceHeight = deviceWidth.height(); // scope.deviceWidth = deviceWidth.width(); // scope.desktopWidth = window.screen.width; // if (scope.deviceWidth < 460) { // scope.isMobile = true; // } else if (scope.deviceWidth < 768) { // scope.isTablet = true; // } else if (scope.desktopWidth < 1440 || scope.deviceWidth < 1440) { // scope.isSmallDesktop = true; // scope.isDesktop = true; // } else { // scope.isDesktop = true; // } // } // }; //}); mod.directive('listScroll', function ($window, $rootScope) { return { restrict: 'EA', link: function (scope, element, attrs) { $('#scroll').click(function () { $('#myshowcases-list').animate({ scrollTop: '+=100' }, 100); }); } }; }); //directive to determine if the file is valid (file field input) mod.directive('validFile', ['$timeout', function ($timeout) { return { require: 'ngModel', link: function (scope, el, attrs, ngModel) { el.bind('change', function () { $timeout(function () { scope.$apply(function () { ngModel.$setViewValue(el.val()); ngModel.$render(); }); }); }); } } }]); //a common date formatting filter mod.filter('dateFormat', function ($filter) { return function (input) { if (input == null) return ""; var date = $filter('date')(new Date(input), 'MMM dd yyyy'); return date.toUpperCase(); }; }); //a common time formatting filter mod.filter('time', function ($filter) { return function (input) { if (input == null) return ""; var date = $filter('date')(new Date(input), 'HH:mm:ss'); return date.toUpperCase(); }; }); mod.filter('truncate', function () { return function (text, length, end) { if (!text) return ''; if (isNaN(length)) length = 10; if (end === undefined || end === null) end = "..."; //add ellipsis if (text.length <= length || text.length - end.length <= length) return text; else return String(text).substring(0, length - end.length) + end; }; }); //format datetime mod.filter('datetime', function ($filter) { return function (input) { if (input == null) return ""; var date = $filter('date')(new Date(input), 'MMM dd yyyy - HH:mm:ss'); return date.toUpperCase(); }; }); //capitolise a string value mod.filter('capitalise', function () { return function (input) { return (!!input) ? input.replace(/([^\W_]+[^\s]*) */g, function (txt) { var split = txt.split('-'); var result = ''; for (var i = 0; i < split.length; i++) { if (split[i]) result = result + split[i].charAt(0).toUpperCase() + split[i].substr(1).toLowerCase(); if (split.length !== i) result = result + ' '; } return result; }) : ''; }; }); mod.filter('startFrom', function () { return function (input, start) { start = +start; //parse to int if (input) { return input.slice(start); } else { return; } } }); //filter items by associated tags mod.filter('tagfilter', function () { return function (templates, tagFilter) { if (!templates || templates === undefined) return null; if (!tagFilter) return templates; var filtered = []; if (tagFilter === 'absolutely no tags whatsoever' && templates !== undefined) { for (var i = 0; i < items.length; i++) { var item = templates[i]; if (item.tags) { if (item.tags.length === 0) filtered.push(item); } } } else { for (var i = 0; i < templates.length; i++) { var item = templates[i]; if (item.tags) { for (var j = 0; j < item.tags.length; j++) { var text = item.tags[j]; if (text === tagFilter) filtered.push(item); } } } } return filtered; }; }); //filter items by associated tags mod.filter('badgeTagFilter', function () { return function (templates, tagFilter) { if (!templates || templates === undefined) return null; if (!tagFilter) return templates; var filtered = []; if (tagFilter === 'absolutely no tags whatsoever' && templates !== undefined) { for (var i = 0; i < items.length; i++) { var item = templates[i]; if (item.tags) { if (item.tags.length === 0) filtered.push(item); } } } else { for (var i = 0; i < templates.length; i++) { var item = templates[i]; if (item.tags) { for (var j = 0; j < item.tags.length; j++) { var text = item.tags[j]; if (text === tagFilter) filtered.push(item); } } } } return filtered; }; }); // Template style renderer mod.directive('styler', ['$compile', function ($compile) { return { restrict: 'E', link: function postLink(scope, element) { if (element.html()) { var template = $compile(''); element.replaceWith(template(scope)); } } }; }]); //display utc date as a locale ready date (according to browser settings) mod.filter('utcdate', ['$filter', function ($filter) { return function (input, format) { if (!angular.isDefined(format)) format = 'MMM d, y h:mm:ss a'; var date = new Date(input); var utc = new Date(date.getFullYear(), date.getMonth(), date.getDate(), date.getHours(), date.getMinutes(), date.getSeconds()); return $filter('date')(utc, format); }; }]); mod.factory('usefulService', [usefulService]); function usefulService() { var service = { dynamicSort: dynamicSort }; return service; function dynamicSort(property) { var sortOrder = 1; //a '-' character is used to denote sort order if (property[0] === "-") { sortOrder = -1; property = property.substr(1); } return function (a, b) { var result = (a[property] < b[property]) ? -1 : (a[property] > b[property]) ? 1 : 0; return result * sortOrder; } } } })(); (function (module) { //file helper functions so that file functionality works nicely with Angular, promises etc. var fileReader = function ($q, $log) { var onLoad = function (reader, deferred, scope) { return function () { scope.$apply(function () { deferred.resolve(reader.result); }); }; }; var onError = function (reader, deferred, scope) { return function () { scope.$apply(function () { deferred.reject(reader.result); }); }; }; var onProgress = function (reader, scope) { return function (event) { scope.$broadcast("fileProgress", { total: event.total, loaded: event.loaded }); }; }; var getReader = function (deferred, scope) { var reader = new FileReader(); reader.onload = onLoad(reader, deferred, scope); reader.onerror = onError(reader, deferred, scope); reader.onprogress = onProgress(reader, scope); return reader; }; //read a file as a binrary string var readAsBinaryString = function (file, scope) { var deferred = $q.defer(); var reader = getReader(deferred, scope); if (file) reader.readAsBinaryString(file); return deferred.promise; }; //read a file as a data url var readAsDataURL = function (file, scope) { var deferred = $q.defer(); var reader = getReader(deferred, scope); reader.readAsDataURL(file); return deferred.promise; }; //read a text file var readAsText = function (file, scope) { var deferred = $q.defer(); var reader = getReader(deferred, scope); if (file) reader.readAsText(file); return deferred.promise; }; return { readAsDataUrl: readAsDataURL, readAsText: readAsText, readAsBinaryString: readAsBinaryString }; }; module.factory("fileReader", ["$q", "$log", fileReader]); }(angular.module("usefulstuff")));