UNPKG

@momsfriendlydevco/angular-ui-history

Version:
771 lines (689 loc) 46.2 kB
"use strict"; angular.module('angular-ui-history', ['angular-bs-tooltip', 'ngQuill', 'ui.gravatar']).factory('$debounce', ['$timeout', function ($timeout) { function debounce(callback, timeout, apply) { timeout = angular.isUndefined(timeout) ? 0 : timeout; apply = angular.isUndefined(apply) ? true : apply; var callCount = 0; return function () { var self = this; var args = arguments; callCount++; var wrappedCallback = function (version) { return function () { if (version === callCount) return callback.apply(self, args); }; }(callCount); return $timeout(wrappedCallback, timeout, apply); }; } return debounce; }]) // Provider {{{ .provider('uiHistory', function () { this.defaults = {}; this.$get = function () { return this; }; }) // }}} // uiHistory (directive) {{{ .component('uiHistory', { bindings: { allowPost: '<?', allowDelete: '<?', allowUpload: '<?', allowUploadList: '<?', buttons: '<?', display: '<?', posts: '<?', queryUrl: '<?', postUrl: '<?', deleteUrl: '<?', templates: '<?', onError: '&?', onLoadingStart: '&?', onLoadingStop: '&?', onQuery: '&?', onUpload: '&?', onUploadStart: '&?', onUploadProgress: '&?', onUploadEnd: '&?', tags: '<?', userAvatar: '@?', baseUrlImage: '<?', mentionUrl: '<?' }, template: "\n\t\t<div class=\"ui-history\">\n\t\t\t<!-- Modal: Upload list {{{ -->\n\t\t\t<div class=\"modal fade angular-ui-history-modal-uploadList\">\n\t\t\t\t<div ng-if=\"$ctrl.showFilesModal\" class=\"modal-dialog\">\n\t\t\t\t\t<div class=\"modal-content\">\n\t\t\t\t\t\t<div class=\"modal-header\">\n\t\t\t\t\t\t\t<a class=\"close\" data-dismiss=\"modal\"><i class=\"fa fa-times\"></i></a>\n\t\t\t\t\t\t\t<h4 class=\"modal-title\">Upload List</h4>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t\t<div class=\"modal-body\">\n\t\t\t\t\t\t\t<ui-history-files\n\t\t\t\t\t\t\t\tallow-upload=\"$ctrl.allowUpload\"\n\t\t\t\t\t\t\t\tquery-url=\"$ctrl.queryUrl\"\n\t\t\t\t\t\t\t\tpost-url=\"$ctrl.postUrl\"\n\t\t\t\t\t\t\t\ton-error=\"$ctrl.onError\"\n\t\t\t\t\t\t\t\ton-loading-start=\"$ctrl.onLoadingStart\"\n\t\t\t\t\t\t\t\ton-loading-stop=\"$ctrl.onLoadingStop\"\n\t\t\t\t\t\t\t\ton-query=\"$ctrl.onQuery\"\n\t\t\t\t\t\t\t\ton-upload=\"$ctrl.onUpload\"\n\t\t\t\t\t\t\t></ui-history-files>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t</div>\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t\t<!-- }}} -->\n\n\t\t\t<ui-history-upload\n\t\t\t\tquery-url=\"$ctrl.queryUrl\"\n\t\t\t\tpost-url=\"$ctrl.postUrl\"\n\t\t\t\ton-error=\"$ctrl.onError\"\n\t\t\t\ton-upload=\"$ctrl.onUpload\"\n\t\t\t\ton-upload-start=\"$ctrl.onUploadStart\"\n\t\t\t\ton-upload-progress=\"$ctrl.onUploadProgress\"\n\t\t\t\ton-upload-end=\"$ctrl.onUploadEnd\"\n\t\t\t\ttags=\"$ctrl.tags\"\n\t\t\t></ui-history-upload>\n\n\t\t\t<!-- Header editor (if display='recentFirst') {{{ -->\n\t\t\t<div ng-if=\"$ctrl.allowPost && $ctrl.display == 'recentFirst'\">\n\t\t\t\t<div ng-show=\"$ctrl.isPosting\" class=\"text-center\">\n\t\t\t\t\t<h3><i class=\"fa fa-spinner fa-spin\"></i> Posting...</h3>\n\t\t\t\t</div>\n\t\t\t\t<div ng-show=\"!$ctrl.isPosting\">\n\t\t\t\t\t<ui-history-editor\n\t\t\t\t\t\tbuttons=\"$ctrl.buttons\"\n\t\t\t\t\t\ttemplates=\"$ctrl.templates\"\n\t\t\t\t\t\ton-post=\"$ctrl.makePost(body, tags, mentions)\"\n\t\t\t\t\t\ttags=\"$ctrl.tags\"\n\t\t\t\t\t\tbase-url-image=\"$ctrl.baseUrlImage\"\n\t\t\t\t\t\tmention-url=\"$ctrl.mentionUrl\"\n\t\t\t\t\t></ui-history-editor>\n\t\t\t\t</div>\n\t\t\t\t<hr/>\n\t\t\t</div>\n\t\t\t<!-- }}} -->\n\t\t\t<div ng-repeat=\"post in $ctrl.posts | orderBy:'date':$ctrl.display=='recentFirst' track by (post.date + post._id)\" ng-switch=\"post.type\" class=\"ui-history-item\">\n\t\t\t\t<div class=\"ui-history-meta\">\n\t\t\t\t\t<div ng-if=\"$ctrl.allowDelete\" class=\"ui-history-delete-post\" tooltip=\"Delete\" ng-click=\"$ctrl.deletePost(post._id)\"><i class=\"fa fa-trash-o\" aria-hidden=\"true\"></i></div>\n\t\t\t\t\t<div ng-if=\"post.tags && post.tags.length\" class=\"ui-history-tags\">\n\t\t\t\t\t\t<span ng-repeat=\"tag in post.tags\" class=\"ui-history-tag\">\n\t\t\t\t\t\t\t{{tag}}\n\t\t\t\t\t\t</span>\n\t\t\t\t\t</div>\n\t\t\t\t\t<div class=\"ui-history-timestamp\" tooltip=\"{{post.date | date:'medium'}}\">\n\t\t\t\t\t\t{{post.date | uiHistoryDate}}\n\t\t\t\t\t</div>\n\t\t\t\t</div>\n\t\t\t\t<!-- type=user.change {{{ -->\n\t\t\t\t<div ng-switch-when=\"user.change\" class=\"ui-history-user-change\">\n\t\t\t\t\t<div class=\"ui-history-user-change-user\">\n\t\t\t\t\t\t<user-history-avatar user=\"post.user\" user-avatar=\"{{$ctrl.userAvatar}}\" default-image=\"{{$ctrl.gravatarDefault}}\"></user-history-avatar>\n\t\t\t\t\t</div>\n\t\t\t\t\t<div ng-if=\"post.field\" class=\"ui-history-user-change-main\">\n\t\t\t\t\t\tChanged\n\t\t\t\t\t\t{{post.field}}\n\t\t\t\t\t\t<em>{{post.from}}</em>\n\t\t\t\t\t\t<i class=\"fa fa-long-arrow-right\"></i>\n\t\t\t\t\t\t<em>{{post.to}}</em>\n\t\t\t\t\t</div>\n\t\t\t\t\t<div ng-if=\"post.fields\" class=\"ui-history-user-change-main\">\n\t\t\t\t\t\tChanges:\n\t\t\t\t\t\t<div ng-repeat=\"(field, change) in post.fields track by field\">\n\t\t\t\t\t\t\t{{field}}\n\t\t\t\t\t\t\t<em>{{change.from}}</em>\n\t\t\t\t\t\t\t<i class=\"fa fa-long-arrow-right\"></i>\n\t\t\t\t\t\t\t<em>{{change.to}}</em>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t</div>\n\t\t\t\t</div>\n\t\t\t\t<!-- }}} -->\n\t\t\t\t<!-- type=user.comment {{{ -->\n\t\t\t\t<div ng-switch-when=\"user.comment\" class=\"ui-history-user-comment\">\n\t\t\t\t\t<div class=\"ui-history-user-comment-user\">\n\t\t\t\t\t\t<user-history-avatar user=\"post.user\" user-avatar=\"{{$ctrl.userAvatar}}\" default-image=\"{{$ctrl.gravatarDefault}}\"></user-history-avatar>\n\t\t\t\t\t</div>\n\t\t\t\t\t<div class=\"ui-history-user-comment-main\">\n\t\t\t\t\t\t<div ng-if=\"post.user.company\" class=\"ui-history-company\">\n\t\t\t\t\t\t\t{{post.user.company}}\n\t\t\t\t\t\t</div>\n\t\t\t\t\t\t<div ng-if=\"post.title\" class=\"ui-history-user-comment-header\">{{post.title}}</div>\n\t\t\t\t\t\t<div class=\"ui-history-user-comment-body\" ng-bind-html=\"post.body\"></div>\n\t\t\t\t\t</div>\n\t\t\t\t</div>\n\t\t\t\t<!-- }}} -->\n\t\t\t\t<!-- type=user.upload {{{ -->\n\t\t\t\t<div ng-switch-when=\"user.upload\" class=\"ui-history-user-upload\">\n\t\t\t\t\t<div class=\"ui-history-user-upload-user\">\n\t\t\t\t\t\t<user-history-avatar user=\"post.user\" user-avatar=\"{{$ctrl.userAvatar}}\" default-image=\"{{$ctrl.gravatarDefault}}\"></user-history-avatar>\n\t\t\t\t\t</div>\n\t\t\t\t\t<div class=\"ui-history-user-upload-main\">\n\t\t\t\t\t\t<div style=\"margin-bottom: 10px\">Attached files:</div>\n\t\t\t\t\t\t<ul class=\"list-group\">\n\t\t\t\t\t\t\t<a ng-if=\"post.filename\" ng-href=\"{{post.url}}\" target=\"_blank\" class=\"list-group-item\">\n\t\t\t\t\t\t\t\t<div ng-if=\"post.size\" class=\"pull-right\">{{post.size}}</div>\n\t\t\t\t\t\t\t\t<i ng-if=\"post.icon\" class=\"{{post.icon}}\"></i>\n\t\t\t\t\t\t\t\t{{post.filename || 'Unknown file'}}\n\t\t\t\t\t\t\t</a>\n\t\t\t\t\t\t\t<a ng-if=\"post.files\" ng-repeat=\"file in post.files track by file.filename\" ng-href=\"{{file.url}}\" target=\"_blank\" class=\"list-group-item\">\n\t\t\t\t\t\t\t\t<div ng-if=\"file.size\" class=\"pull-right\">{{file.size}}</div>\n\t\t\t\t\t\t\t\t<i ng-if=\"file.icon\" class=\"{{file.icon}}\"></i>\n\t\t\t\t\t\t\t\t{{file.filename || 'Unknown file'}}\n\t\t\t\t\t\t\t</a>\n\t\t\t\t\t\t</ul>\n\t\t\t\t\t</div>\n\t\t\t\t</div>\n\t\t\t\t<!-- }}} -->\n\t\t\t\t<!-- type=user.status {{{ -->\n\t\t\t\t<div ng-switch-when=\"user.status\" class=\"ui-history-user-status\">\n\t\t\t\t\t<div class=\"ui-history-user-status-user\">\n\t\t\t\t\t\t<user-history-avatar user=\"post.user\" user-avatar=\"{{$ctrl.userAvatar}}\" default-image=\"{{$ctrl.gravatarDefault}}\"></user-history-avatar>\n\t\t\t\t\t</div>\n\t\t\t\t\t<div class=\"ui-history-user-status-main\" ng-bind-html=\"post.body\"></div>\n\t\t\t\t</div>\n\t\t\t\t<!-- }}} -->\n\t\t\t\t<!-- type=system.change {{{ -->\n\t\t\t\t<div ng-switch-when=\"system.change\" class=\"ui-history-system-change\">\n\t\t\t\t\t<div ng-if=\"post.field\">\n\t\t\t\t\t\tChanged\n\t\t\t\t\t\t{{post.field}}\n\t\t\t\t\t\t<em>{{post.from}}</em>\n\t\t\t\t\t\t<i class=\"fa fa-long-arrow-right\"></i>\n\t\t\t\t\t\t<em>{{post.to}}</em>\n\t\t\t\t\t</div>\n\t\t\t\t\t<div ng-if=\"post.fields\">\n\t\t\t\t\t\tChanges:\n\t\t\t\t\t\t<div ng-repeat=\"(field, change) in post.fields track by field\">\n\t\t\t\t\t\t\t{{field}}\n\t\t\t\t\t\t\t<em>{{change.from}}</em>\n\t\t\t\t\t\t\t<i class=\"fa fa-long-arrow-right\"></i>\n\t\t\t\t\t\t\t<em>{{change.to}}</em>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t</div>\n\t\t\t\t</div>\n\t\t\t\t<!-- }}} -->\n\t\t\t\t<!-- type=system.status {{{ -->\n\t\t\t\t<div ng-switch-when=\"system.status\" class=\"ui-history-system-status\" ng-bind-html=\"post.body\"></div>\n\t\t\t\t<!-- }}} -->\n\t\t\t\t<!-- type unknown {{{ -->\n\t\t\t\t<div ng-switch-default class=\"ui-history-unknown\">\n\t\t\t\t\tUnknown history type: [{{post.type}}]\n\t\t\t\t</div>\n\t\t\t\t<!-- }}} -->\n\t\t\t</div>\n\t\t\t<div ng-if=\"!$ctrl.posts.length\" class=\"text-muted text-center\">No history to display</div>\n\t\t\t<!-- Footer editor (if !display || display='oldestFirst') {{{ -->\n\t\t\t<div ng-if=\"$ctrl.allowPost && (!$ctrl.display || $ctrl.display == 'oldestFirst')\">\n\t\t\t\t<hr/>\n\t\t\t\t<div ng-show=\"$ctrl.isPosting\" class=\"text-center\">\n\t\t\t\t\t<h3><i class=\"fa fa-spinner fa-spin\"></i> Posting...</h3>\n\t\t\t\t</div>\n\t\t\t\t<div ng-show=\"!$ctrl.isPosting\">\n\t\t\t\t\t<ui-history-editor\n\t\t\t\t\t\tbuttons=\"$ctrl.buttons\"\n\t\t\t\t\t\ttemplates=\"$ctrl.templates\"\n\t\t\t\t\t\ton-post=\"$ctrl.makePost(body, tags, mentions)\"\n\t\t\t\t\t\tbase-url-image=\"$ctrl.baseUrlImage\"\n\t\t\t\t\t\tmention-url=\"$ctrl.mentionUrl\"\n\t\t\t\t\t></ui-history-editor>\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t\t<!-- }}} -->\n\t\t</div>\n\t", controller: ["$element", "$http", "$q", "$rootScope", "$sce", "$scope", "$timeout", "uiHistory", function controller($element, $http, $q, $rootScope, $sce, $scope, $timeout, uiHistory) { var $ctrl = this; // refresh + .posts - History display + fetcher {{{ $ctrl.posts; $ctrl.isLoading = false; /** * Load all posts (either via the queryUrl or from the posts array) * @returns {Promise} */ $ctrl.refresh = function () { var force = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : false; if (!force && $ctrl.posts) return $q.resolve(); // User is supplying the post collection rather than us fetching it - do nothing $q.resolve() // Pre loading phase {{{ .then(function () { return $ctrl.isLoading = true; }).then(function () { if (angular.isFunction($ctrl.onLoadingStart)) return $ctrl.onLoadingStart(); }) // }}} // Data fetching - either via queryUrl or examining posts {{{ .then(function () { if (angular.isString($ctrl.queryUrl) || angular.isFunction($ctrl.queryUrl)) { var resolvedUrl = angular.isString($ctrl.queryUrl) ? $ctrl.queryUrl : $ctrl.queryUrl($ctrl); if (!resolvedUrl) throw new Error('Resovled URL is empty'); return $http.get(resolvedUrl).then(function (res) { if (!angular.isArray(res.data)) { throw new Error("Expected history feed at URL \"".concat(resolvedUrl, "\" to be an array but got something else")); } else { return res.data; } }); } else if (angular.isArray($ctrl.posts)) { return $ctrl.posts; } else { throw new Error('Cannot refresh posts - neither queryUrl (func / array) or posts (array) are specified'); } }) // }}} // Misc data mangling {{{ .then(function (data) { $ctrl.posts = data.map(function (post) { if (typeof post.body === 'string' && (post.type == 'user.comment' || post.type == 'user.status' || post.type == 'system.status')) post.body = $sce.trustAsHtml(post.body); return post; }); }) // }}} // If user has a onQuery handler wait for it to mangle / filter the data {{{ .then(function () { if ($ctrl.onQuery) { var res = $ctrl.onQuery({ posts: $ctrl.posts }); if (angular.isArray(res)) $ctrl.posts = res; } }) // }}} // Post loading + catchers {{{ ["catch"](function (error) { if (angular.isFunction($ctrl.onError)) $ctrl.onError({ error: error }); })["finally"](function () { return $ctrl.isLoading = false; })["finally"](function () { if (angular.isFunction($ctrl.onLoadingStop)) return $ctrl.onLoadingStop(); }); // }}} }; $scope.$on('angular-ui-history.refresh', function () { return $ctrl.refresh(true); }); // }}} // .makePost - New post contents {{{ $ctrl.isPosting = false; $ctrl.makePost = function (body, tags, mentions) { if (!$ctrl.allowPost) throw new Error('Posting not allowed'); if (!body) { // Silently forget if the user is trying to publish empty contents $rootScope.$broadcast('angular-ui-history.empty-post'); return; } var resolvedUrl = angular.isString($ctrl.postUrl) ? $ctrl.postUrl : angular.isFunction($ctrl.postUrl) ? $ctrl.postUrl($ctrl) : angular.isString($ctrl.queryUrl) ? $ctrl.queryUrl : angular.isFunction($ctrl.queryUrl) ? $ctrl.queryUrl($ctrl) : undefined; if (!resolvedUrl) throw new Error('Resolved POST URL is empty'); return $q.resolve().then(function () { return $ctrl.isPosting = true; }).then(function () { return $http.post(resolvedUrl, { body: body, tags: tags }); }).then(function () { return $ctrl.refresh(true); }).then(function () { return $rootScope.$broadcast('angular-ui-history.posted', body, mentions); })["catch"](function (error) { if ($ctrl.onError) $ctrl.onError({ error: error }); })["finally"](function () { return $ctrl.isPosting = false; }); }; // }}} // Delete posts {{{ $ctrl.isDeleting = false; $ctrl.deletePost = function (id) { if (!$ctrl.allowDelete) throw new Error('Deleting not allowed'); if (!id) return; var resolvedUrl = angular.isString($ctrl.deleteUrl) ? $ctrl.deleteUrl : angular.isFunction($ctrl.deleteUrl) ? $ctrl.deleteUrl($ctrl) : undefined; if (!resolvedUrl) throw new Error('Resolved DELETE URL is empty'); return $q.resolve().then(function () { return $ctrl.isDeleting = true; }).then(function () { return $http["delete"]("".concat(resolvedUrl, "/").concat(id)); }).then(function () { return $ctrl.refresh(true); }).then(function () { return $rootScope.$broadcast('angular-ui-history.deleted', id); })["catch"](function (error) { if ($ctrl.onError) $ctrl.onError({ error: error }); })["finally"](function () { return $ctrl.isDeleting = false; }); }; // }}} // Trigger the file upload dialog using the upload helper input element {{{ $scope.$on('angular-ui-history.button.upload', function () { return $ctrl.selectFiles(); }); $ctrl.selectFiles = function () { return $element.find('input[type=file]').click(); }; // }}} // File upload list {{{ $ctrl.showFilesModal = false; $scope.$on('angular-ui-history.button.uploadList', function () { $ctrl.showFilesModal = true; $element.find('.angular-ui-history-modal-uploadList').modal('show').one('hidden.bs.modal', function () { return $scope.$apply(function () { return $ctrl.showFilesModal = true; }); }); }); // }}} // Init {{{ $ctrl.$onInit = function () { // Apply defaults for (var key in uiHistory.defaults) { if ($ctrl[key] === undefined) $ctrl[key] = uiHistory.defaults[key]; } // Buttons is empty fill it with something appropriate based on settings if (!$ctrl.buttons) { $ctrl.buttons = []; if ($ctrl.allowUpload) { if ($ctrl.allowUploadList === undefined || $ctrl.allowUploadList) $ctrl.buttons.push({ title: 'File list', icon: 'fa fa-folder-o', action: 'uploadList' }); $ctrl.buttons.push({ title: 'Upload files...', icon: 'fa fa-file', action: 'upload' }); } ; } // Watch the queryUrl - this fires initially to refresh everything but will also respond to changes by causing a refresh $scope.$watch('$ctrl.queryUrl', function () { return $ctrl.refresh(true); }); }; // }}} }] }) // }}} // uiHistoryEditor (directive, post comment area) {{{ /** * The post area WYSIWYG editor for angular-ui-history * @param {function} onPost The function to be called when the user has finished writing text. If this is a promise the input will be cleared if it resolves correctly * @param {array} [buttons] A collection of buttons to display in the editor. Each should have at least `title` and `onClick` with optional `icon`, `class` */ .component('uiHistoryEditor', { bindings: { buttons: '<', templates: '<', onPost: '&', tags: '<?', baseUrlImage: '<?', mentionUrl: '<?' }, template: "\n\t\t<form ng-submit=\"$ctrl.makePost()\" class=\"form-horizontal\">\n\t\t\t<div class=\"form-group\">\n\t\t\t\t<div class=\"col-sm-12\">\n\t\t\t\t\t<ng-quill-editor ng-model=\"$ctrl.newPost.body\" on-content-changed=\"$ctrl.contentChanged()\" modules=\"$ctrl.modules\" on-editor-created=\"$ctrl.editorCreated(editor)\">\n\t\t\t\t\t\t<!-- ng-quill toolbar config {{{ -->\n\t\t\t\t\t\t<ng-quill-toolbar>\n\t\t\t\t\t\t\t<div>\n\t\t\t\t\t\t\t\t<span class=\"ql-formats\">\n\t\t\t\t\t\t\t\t\t<button class=\"ql-bold\" ng-attr-title=\"{{'Bold'}}\"></button>\n\t\t\t\t\t\t\t\t\t<button class=\"ql-italic\" ng-attr-title=\"{{'Italic'}}\"></button>\n\t\t\t\t\t\t\t\t\t<button class=\"ql-underline\" ng-attr-title=\"{{'Underline'}}\"></button>\n\t\t\t\t\t\t\t\t\t<button class=\"ql-strike\" ng-attr-title=\"{{'Strikethough'}}\"></button>\n\t\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t\t\t<span class=\"ql-formats\">\n\t\t\t\t\t\t\t\t\t<button class=\"ql-blockquote\" ng-attr-title=\"{{'Block Quote'}}\"></button>\n\t\t\t\t\t\t\t\t\t<button class=\"ql-code-block\" ng-attr-title=\"{{'Code Block'}}\"></button>\n\t\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t\t\t<span class=\"ql-formats\">\n\t\t\t\t\t\t\t\t\t<button class=\"ql-header\" value=\"1\" ng-attr-title=\"{{'Header 1'}}\"></button>\n\t\t\t\t\t\t\t\t\t<button class=\"ql-header\" value=\"2\" ng-attr-title=\"{{'Header 2'}}\"></button>\n\t\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t\t\t<span class=\"ql-formats\">\n\t\t\t\t\t\t\t\t\t<button class=\"ql-list\" value=\"ordered\" ng-attr-title=\"{{'Numered list'}}\"></button>\n\t\t\t\t\t\t\t\t\t<button class=\"ql-list\" value=\"bullet\" ng-attr-title=\"{{'Bulleted list'}}\"></button>\n\t\t\t\t\t\t\t\t\t<button class=\"ql-indent\" value=\"-1\" ng-attr-title=\"{{'De-indent'}}\"></button>\n\t\t\t\t\t\t\t\t\t<button class=\"ql-indent\" value=\"+1\" ng-attr-title=\"{{'Indent'}}\"></button>\n\t\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t\t\t<span class=\"ql-formats\">\n\t\t\t\t\t\t\t\t\t<select class=\"ql-color\" ng-attr-title=\"{{'Text color'}}\">\n\t\t\t\t\t\t\t\t\t\t<option selected=\"selected\"></option>\n\t\t\t\t\t\t\t\t\t\t<option ng-repeat=\"color in ::$ctrl.colors\" value=\"{{::color}}\"></option>\n\t\t\t\t\t\t\t\t\t</select>\n\t\t\t\t\t\t\t\t\t<select class=\"ql-background\" ng-attr-title=\"{{'Background color'}}\">\n\t\t\t\t\t\t\t\t\t\t<option selected=\"selected\"></option>\n\t\t\t\t\t\t\t\t\t\t<option ng-repeat=\"color in ::$ctrl.colors\" value=\"{{::color}}\"></option>\n\t\t\t\t\t\t\t\t\t</select>\n\t\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t\t\t<span class=\"ql-formats\">\n\t\t\t\t\t\t\t\t\t<button class=\"ql-link\" value=\"ordered\" ng-attr-title=\"{{'Add link'}}\"></button>\n\t\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t\t\t<span class=\"ql-formats\">\n\t\t\t\t\t\t\t\t\t<button class=\"ql-clean\" value=\"ordered\" ng-attr-title=\"{{'Clear formatting'}}\"></button>\n\t\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t\t\t<div class=\"pull-right\">\n\t\t\t\t\t\t\t\t\t<div class=\"btn-group\">\n\t\t\t\t\t\t\t\t\t\t<a ng-if=\"$ctrl.templates\" class=\"btn btn-default dropdown-toggle\" data-toggle=\"dropdown\">\n\t\t\t\t\t\t\t\t\t\t\tTemplate\n\t\t\t\t\t\t\t\t\t\t\t<i class=\"fa fa-chevron-down\"></i>\n\t\t\t\t\t\t\t\t\t\t</a>\n\t\t\t\t\t\t\t\t\t\t<ul class=\"dropdown-menu\">\n\t\t\t\t\t\t\t\t\t\t\t<li ng-repeat=\"template in $ctrl.templates\">\n\t\t\t\t\t\t\t\t\t\t\t\t<a ng-click=\"$ctrl.setTemplate(template)\">\n\t\t\t\t\t\t\t\t\t\t\t\t\t<i ng-if=\"template.icon\" ng-class=\"template.icon\"></i>\n\t\t\t\t\t\t\t\t\t\t\t\t\t{{template.title}}\n\t\t\t\t\t\t\t\t\t\t\t\t</a>\n\t\t\t\t\t\t\t\t\t\t\t</li>\n\t\t\t\t\t\t\t\t\t\t</ul>\n\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t<a ng-repeat=\"button in $ctrl.buttons\" class=\"btn\" ng-class=\"button.class || 'btn-default'\" ng-click=\"$ctrl.runButton(button)\">\n\t\t\t\t\t\t\t\t\t\t<i ng-if=\"button.icon\" class=\"{{button.icon}}\"></i>\n\t\t\t\t\t\t\t\t\t\t{{button.title}}\n\t\t\t\t\t\t\t\t\t</a>\n\t\t\t\t\t\t\t\t\t<a ng-click=\"$ctrl.makePost()\" class=\"btn btn-sm btn-success\">\n\t\t\t\t\t\t\t\t\t\t<i class=\"fa fa-plus\"></i>\n\t\t\t\t\t\t\t\t\t\tPost\n\t\t\t\t\t\t\t\t\t</a>\n\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t</ng-quill-toolbar>\n\t\t\t\t\t\t<!-- }}} -->\n\t\t\t\t\t</ng-quill-editor>\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t</form>\n\t", controller: ["$scope", "$q", "$element", "$debounce", "$http", function controller($scope, $q, $element, $debounce, $http) { var $ctrl = this; // Quill setup {{{ $ctrl.modules = {}; $ctrl.colors = ['#e60000', '#ff9900', '#ffff00', '#008a00', '#0066cc', '#9933ff', '#ffffff', '#facccc', '#ffebcc', '#ffffcc', '#cce8cc', '#cce0f5', '#ebd6ff', '#bbbbbb', '#f06666', '#ffc266', '#ffff66', '#66b966', '#66a3e0', '#c285ff', '#888888', '#a10000', '#b26b00', '#b2b200', '#006100', '#0047b2', '#6b24b2', '#444444', '#5c0000', '#663d00', '#666600', '#003700', '#002966', '#3d1466'], $ctrl.contentChanged = $debounce(function () { $q.resolve().then(function () { return $ctrl.baseUrlImage ? null : $q.reject('SKIP'); }).then(function () { return Array.from($element[0].querySelectorAll('img:not([src^="data:"])')); }).then(function (imgs) { return $q.all(imgs.map(function (img) { return $q.resolve().then(function () { return img.classList.add('img--loading'); }).then(function () { return $ctrl.convertImgToBase64URL(img.getAttribute('src')); }).then(function (res) { return img.setAttribute('src', res); })["finally"](function () { return img.classList.remove('img--loading'); }); })); })["catch"](function (err) { return err == 'SKIP' ? null : $q.reject(err); })["finally"](function () { return $scope.$emit('angular-ui-history.content', $ctrl.newPost.body); }); }, 300); $ctrl.convertImgToBase64URL = function (url) { return new Promise(function (resolve, reject) { var canvas = document.createElement('CANVAS'); var img = document.createElement('img'); img.setAttribute('src', url); img.setAttribute('crossorigin', 'anonymous'); img.onload = function () { canvas.height = img.height; canvas.width = img.width; var ctx = canvas.getContext('2d'); ctx.drawImage(img, 10, 10, img.width, img.height); resolve(canvas.toDataURL()); }; img.onerror = function (error) { return reject('Could not load image, please check that the file is accessible'); }; }); }; $ctrl.editorCreated = function (quill) { return $ctrl.quill = quill; }; $ctrl.mentionList = function () { var uniqueMentionList = {}; $ctrl.quill.getContents().ops.filter(function (op) { return op.insert && op.insert.mention; }).map(function (op) { return op.insert.mention; }).forEach(function (mention) { if (!uniqueMentionList[mention._id]) { uniqueMentionList[mention._id] = { _id: mention._id, name: mention.value }; } }); return Object.values(uniqueMentionList); }; // }}} $ctrl.newPost = { body: '' }; $ctrl.makePost = function () { if (!$ctrl.onPost) throw new Error('Post content provided but no onPost binding defined'); var ret = $ctrl.onPost({ body: $ctrl.newPost.body, tags: $ctrl.newPost.tags, mentions: $ctrl.mentionUrl ? $ctrl.mentionList() : [] }); if (!ret) return; // Didn't return a promise - ignore if (angular.isFunction(ret.then)) ret.then(function () { $ctrl.newPost = { body: '' }; if ($ctrl.tags && $ctrl.tags.length) $ctrl.newPost.tags = $ctrl.tags.map(function (t) { return t; }); }); }; $scope.$on('angular-ui-history.post', $ctrl.makePost); /** * Execute the event bubbling for the given button * @param {Object} button The button object that triggered the event * @fires angular-ui-history.button.${button.action} * @fires angular-ui-history.button */ $ctrl.runButton = function (button) { if (button.action) $scope.$emit("angular-ui-history.button.".concat(button.action), button); $scope.$emit('angular-ui-history.button', button); }; /** * Select a template to use * @param {Object} template The selected template object * @fires angular-ui-history.template.${template} */ $ctrl.setTemplate = function (template) { $ctrl.newPost.body = template.content; $scope.$emit('angular-ui-history.template', template); }; // Init {{{ $ctrl.$onInit = function () { // Initialize tags to for new comments if ($ctrl.tags && $ctrl.tags.length) $ctrl.newPost.tags = $ctrl.tags.map(function (t) { return t; }); // Drag/drop images if ($ctrl.baseUrlImage) { $ctrl.modules.imageDrop = true; } if ($ctrl.mentionUrl) { $ctrl.modules.mention = { allowedChars: /^[A-Za-z\sÅÄÖåäö]*$/, mentionDenotationChars: ['@'], dataAttributes: ['_id'], source: function source(searchTerm, renderList) { return $q.resolve().then(function () { if (angular.isString($ctrl.mentionUrl) || angular.isFunction($ctrl.mentionUrl)) { var resolvedUrl = angular.isString($ctrl.mentionUrl) ? $ctrl.mentionUrl : $ctrl.mentionUrl(searchTerm); if (!resolvedUrl) return $q.reject(new Error('Resovled URL is empty')); return $http.get(resolvedUrl).then(function (res) { if (!angular.isArray(res.data)) { throw new Error("Expected mention feed at URL \"".concat(resolvedUrl, "\" to be an array but got something else")); } else { return res.data; } }); } }).then(function (matchedPeople) { return renderList(matchedPeople); })["catch"](console.error); } }; } }; // }}} }] }) // }}} // uiHistoryUpload // {{{ .component('uiHistoryUpload', { bindings: { queryUrl: '<', postUrl: '<', onError: '&?', onUpload: '&?', onUploadStart: '&?', onUploadProgress: '&?', onUploadEnd: '&?', tags: '<?' }, template: "\n\t\t<div style=\"display: none\">\n\t\t\t<input name=\"file\" multiple type=\"file\"/>\n\t\t</div>\n\t", controller: ["$scope", "$element", "$http", "$timeout", function controller($scope, $element, $http, $timeout) { var $ctrl = this; $ctrl.isUploading = false; $ctrl.upload = function () { var _this = this; console.log('upload', $ctrl.postUrl, $ctrl.queryUrl, this.files); $timeout(function () { // Attach to file widget and listen for change events so we can update the text var resolvedUrl = angular.isString($ctrl.postUrl) ? $ctrl.postUrl : angular.isFunction($ctrl.postUrl) ? $ctrl.postUrl($ctrl) : angular.isString($ctrl.queryUrl) ? $ctrl.queryUrl : angular.isFunction($ctrl.queryUrl) ? $ctrl.queryUrl($ctrl) : undefined; if (!resolvedUrl) throw new Error('Resovled POST URL is empty'); var formData = new FormData(); Object.keys(_this.files).forEach(function (k, i) { return formData.append('file_' + i, _this.files[k]); }); // Tag files if ($ctrl.tags && $ctrl.tags.length) { $ctrl.tags.forEach(function (tag) { return formData.append('tags[]', tag); }); } $ctrl.isUploading = true; Promise.resolve().then(function () { if ($ctrl.onUploadStart) $ctrl.onUploadStart({ files: _this.files }); }).then(function () { return $http.post(resolvedUrl, formData, { headers: { 'Content-Type': undefined }, // Need to override the headers so that angular changes them over into multipart/mime transformRequest: angular.identity, uploadEventHandlers: $ctrl.onUploadProgress ? { progress: function progress(e) { $ctrl.onUploadProgress({ files: _this.files, progress: Math.round(e.loaded / e.total * 100) }); } } : undefined }); }).then(function (res) { if ($ctrl.onUpload) $ctrl.onUpload(res); if ($ctrl.onUploadEnd) $ctrl.onUploadEnd({ files: _this.files }); })["catch"](function (error) { if ($ctrl.onError) $ctrl.onError({ error: error }); }).then(function () { return $scope.$emit('angular-ui-history.refresh'); })["finally"](function () { return $ctrl.isUploading = false; }); }); }; $element.find('input[type=file]').on('change', $ctrl.upload); }] }) // }}} // uiHistoryFiles (directive, file listing area) {{{ .component('uiHistoryFiles', { bindings: { buttons: '<', allowUpload: '<', queryUrl: '<', postUrl: '<', onError: '&?', onLoadingStart: '&?', onLoadingStop: '&?', onQuery: '&?', onUpload: '&?', onUploadStart: '&?', onUploadProgress: '&?', onUploadEnd: '&?', tags: '<?' }, controller: ["$http", "$scope", "$element", function controller($http, $scope, $element) { var $ctrl = this; $ctrl.uploads; // Data refresher {{{ $ctrl.isLoading; $ctrl.refresh = function () { if (!$ctrl.queryUrl) throw new Error('queryUrl is undefined'); var resolvedUrl = angular.isString($ctrl.queryUploadsUrl) ? $ctrl.queryUploadsUrl : angular.isString($ctrl.queryUrl) ? $ctrl.queryUrl : $ctrl.queryUrl($ctrl); if (!resolvedUrl) throw new Error('Resovled URL for uploads is empty'); $ctrl.isLoading = true; if ($ctrl.onLoadingStart) $ctrl.onLoadingStart(); $http.get(resolvedUrl).then(function (res) { if (!angular.isArray(res.data)) throw new Error("Expected file upload feed at URL \"".concat(resolvedUrl, "\" to be an array but got something else")); $ctrl.uploads = res.data.filter(function (i) { return i.type === undefined || i.type == 'user.upload'; }) // Filter out non-uploads .reduce(function (uploads, post) { // Compress multiple files into a flattened array if (post.filename) { // Single file uploads.push(post); return uploads; } else if (post.files) { // Multiple files return uploads.concat(post.files); } }, []).sort(function (a, b) { // Sort by filename A-Z if (a.filename == b.filename) return 0; return a.filename > b.filename ? 1 : -1; }).filter(function (i, index, arr) { return index == 0 || arr[index - 1].filename != i.filename; }); // Remove duplicate filenames })["catch"](function (error) { if ($ctrl.onError) $ctrl.onError({ error: error }); })["finally"](function () { return $ctrl.isLoading = false; })["finally"](function () { if ($ctrl.onLoadingStop) $ctrl.onLoadingStop(); }); }; $scope.$on('angular-ui-history.refresh', function () { return $ctrl.refresh(true); }); // Retrieve Selected Files {{{ // TODO: Subscribe to an event instead? Button verbs? $ctrl.getSelectedFiles = function (file) { // TODO: Ability to toggle files before downloading //return $ctrl.uploads.filter(p => p.selected); return $ctrl.uploads; }; // }}} // File Download {{{ $ctrl.downloadFiles = function () { var files = $ctrl.getSelectedFiles(); var link = document.createElement('a'); link.style.display = 'none'; document.body.appendChild(link); for (var i = 0; i < files.length; i++) { link.setAttribute('download', files[i].filename); link.setAttribute('href', files[i].url); link.click(); } document.body.removeChild(link); }; // }}} // Trigger the file upload dialog using the upload helper input element {{{ $scope.$on('angular-ui-history.button.upload', function () { return $ctrl.selectFiles(); }); $ctrl.selectFiles = function () { return $element.find('input[type=file]').click(); }; // }}} /** * Execute the event bubbling for the given button * @param {Object} button The button object that triggered the event * @fires angular-ui-history.button.${button.action} * @fires angular-ui-history.button */ $ctrl.runButton = function (button) { if (button.action) $scope.$emit("angular-ui-history.button.".concat(button.action), button); $scope.$emit('angular-ui-history.button', button); }; // Init {{{ $ctrl.$onInit = function () { // Buttons is empty fill it with something appropriate based on settings if (!$ctrl.buttons) { $ctrl.buttons = []; if ($ctrl.allowUpload) $ctrl.buttons.push({ title: 'Upload files...', icon: 'fa fa-file', "class": 'btn-primary', action: 'upload' }); } }; // }}} $scope.$evalAsync($ctrl.refresh); // }}} }], template: "\n\t\t<ui-history-upload\n\t\t\tquery-url=\"$ctrl.queryUrl\"\n\t\t\tpost-url=\"$ctrl.postUrl\"\n\t\t\ton-error=\"$ctrl.onError\"\n\t\t\ton-upload=\"$ctrl.onUpload\"\n\t\t\ton-upload-start=\"$ctrl.onUploadStart\"\n\t\t\ton-upload-progress=\"$ctrl.onUploadProgress\"\n\t\t\ton-upload-end=\"$ctrl.onUploadEnd\"\n\t\t\ttags=\"$ctrl.tags\"\n\t\t></ui-history-upload>\n\n\t\t<div ng-if=\"$ctrl.isLoading\">\n\t\t\t<h2>\n\t\t\t\t<i class=\"fa fa-spinner fa-spin\"></i>\n\t\t\t\tFetching list of files...\n\t\t\t</h2>\n\t\t</div>\n\t\t<div ng-if=\"!$ctrl.isLoading && $ctrl.uploads.length == 0\" class=\"text-muted text-center\">\n\t\t\tNo file uploads found\n\t\t</div>\n\t\t<ul class=\"list-group\">\n\t\t\t<a ng-repeat=\"file in $ctrl.uploads track by file.filename\" ng-href=\"{{file.url}}\" target=\"_blank\" class=\"list-group-item\">\n\t\t\t\t<div class=\"pull-right\">\n\t\t\t\t\t<span class=\"badge\">{{file.size}}</span>\n\t\t\t\t</div>\n\t\t\t\t{{file.filename}}\n\t\t\t</a>\n\t\t</ul>\n\t\t<div class=\"form-group\">\n\t\t\t<a ng-repeat=\"button in $ctrl.buttons\" class=\"btn\" ng-class=\"button.class || 'btn-default'\" ng-click=\"$ctrl.runButton(button)\">\n\t\t\t\t<i ng-if=\"button.icon\" class=\"{{button.icon}}\"></i>\n\t\t\t\t{{button.title}}\n\t\t\t</a>\n\t\t\t<a ng-click=\"$ctrl.downloadFiles()\" ng-class=\"$ctrl.getSelectedFiles().length > 0?'':'disabled'\" class=\"btn btn-primary\">\n\t\t\t\t<i class=\"fa fa-download\"></i> Download All Files\n\t\t\t</a>\n\t\t</div>\n\t" }) // }}} // uiHistoryLatest // Show the most recent history item // {{{ .component('uiHistoryLatest', { bindings: { queryUrl: '<?', onError: '&?', onLoadingStart: '&?', onLoadingStop: '&?', onQuery: '&?' }, template: "\n\t\t<div class=\"ui-history ui-history-latest\">\n\t\t\t<div class=\"ui-history-item\" ng-switch=\"$ctrl.post.type\" ng-if=\"$ctrl.post\">\n\t\t\t\t<div class=\"ui-history-meta\">\n\t\t\t\t\t<a ng-href=\"{{$ctrl.post.user.url}}\" target=\"_blank\" class=\"ui-history-latest-user\">\n\t\t\t\t\t\t{{$ctrl.post.user.name}}{{$ctrl.post.date ? ',' : ''}}\n\t\t\t\t\t</a>\n\t\t\t\t\t<div class=\"ui-history-timestamp\" >\n\t\t\t\t\t\t{{$ctrl.post.date ? ($ctrl.post.date | uiHistoryDate) : ''}}\n\t\t\t\t\t</div>\n\t\t\t\t</div>\n\n\t\t\t\t<!-- type=user.change {{{ -->\n\t\t\t\t<div ng-switch-when=\"user.change\" class=\"ui-history-user-change\">\n\t\t\t\t\t<div ng-if=\"$ctrl.post.field\" class=\"ui-history-user-change-main\">\n\t\t\t\t\t\t<a ng-href=\"{{$ctrl.post.user.url}}\" target=\"_blank\">\n\t\t\t\t\t\t\t{{$ctrl.post.user.name}}\n\t\t\t\t\t\t</a>\n\t\t\t\t\t\tChanged\n\t\t\t\t\t\t{{$ctrl.post.field}}\n\t\t\t\t\t\t<em>{{$ctrl.post.from}}</em>\n\t\t\t\t\t\t<i class=\"fa fa-long-arrow-right\"></i>\n\t\t\t\t\t\t<em>{{$ctrl.post.to}}</em>\n\t\t\t\t\t</div>\n\t\t\t\t\t<div ng-if=\"$ctrl.post.fields\" class=\"ui-history-user-change-main\">\n\t\t\t\t\t\tChanges:\n\t\t\t\t\t\t<div ng-repeat=\"(field, change) in $ctrl.post.fields track by field\">\n\t\t\t\t\t\t\t{{field}}\n\t\t\t\t\t\t\t<em>{{change.from}}</em>\n\t\t\t\t\t\t\t<i class=\"fa fa-long-arrow-right\"></i>\n\t\t\t\t\t\t\t<em>{{change.to}}</em>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t</div>\n\t\t\t\t</div>\n\t\t\t\t<!-- }}} -->\n\n\t\t\t\t<!-- type=user.comment {{{ -->\n\t\t\t\t<div ng-switch-when=\"user.comment\" class=\"ui-history-user-comment\">\n\t\t\t\t\t<div ng-if=\"$ctrl.post.user.company\" class=\"ui-history-company\">\n\t\t\t\t\t\t{{$ctrl.post.user.company}}\n\t\t\t\t\t</div>\n\t\t\t\t\t<div class=\"ui-history-user-comment-main\">\n\t\t\t\t\t\t<div ng-if=\"$ctrl.post.title\" class=\"ui-history-user-comment-header\">{{$ctrl.post.title}}</div>\n\t\t\t\t\t\t<div class=\"ui-history-user-comment-body\" ng-bind-html=\"$ctrl.post.body\"></div>\n\t\t\t\t\t</div>\n\t\t\t\t</div>\n\t\t\t\t<!-- }}} -->\n\n\t\t\t\t<!-- type=user.upload {{{ -->\n\t\t\t\t<div ng-switch-when=\"user.upload\" class=\"ui-history-user-upload\">\n\t\t\t\t\t<div class=\"ui-history-user-upload-main\">\n\t\t\t\t\t\t<div style=\"margin-bottom: 10px\"><a ng-href=\"{{$ctrl.post.user.url}}\" target=\"_blank\" >{{$ctrl.post.user.name}}</a> attached files:</div>\n\t\t\t\t\t\t<ul class=\"list-group\">\n\t\t\t\t\t\t\t<a ng-if=\"$ctrl.post.filename\" ng-href=\"{{$ctrl.post.url}}\" target=\"_blank\" class=\"list-group-item\">\n\t\t\t\t\t\t\t\t<div ng-if=\"$ctrl.post.size\" class=\"pull-right\">{{$ctrl.post.size}}</div>\n\t\t\t\t\t\t\t\t<i ng-if=\"$ctrl.post.icon\" class=\"{{$ctrl.post.icon}}\"></i>\n\t\t\t\t\t\t\t\t{{$ctrl.post.filename || 'Unknown file'}}\n\t\t\t\t\t\t\t</a>\n\t\t\t\t\t\t\t<a ng-if=\"$ctrl.post.files\" ng-repeat=\"file in $ctrl.post.files track by file.filename\" ng-href=\"{{file.url}}\" target=\"_blank\" class=\"list-group-item\">\n\t\t\t\t\t\t\t\t<div ng-if=\"file.size\" class=\"pull-right\">{{file.size}}</div>\n\t\t\t\t\t\t\t\t<i ng-if=\"file.icon\" class=\"{{file.icon}}\"></i>\n\t\t\t\t\t\t\t\t{{file.filename || 'Unknown file'}}\n\t\t\t\t\t\t\t</a>\n\t\t\t\t\t\t</ul>\n\t\t\t\t\t</div>\n\t\t\t\t</div>\n\t\t\t\t<!-- }}} -->\n\n\t\t\t\t<!-- type=user.status {{{ -->\n\t\t\t\t<div ng-switch-when=\"user.status\" class=\"ui-history-user-status\">\n\t\t\t\t\t<div class=\"ui-history-user-status-main\" ng-bind-html=\"$ctrl.post.body\"></div>\n\t\t\t\t</div>\n\t\t\t\t<!-- }}} -->\n\n\t\t\t\t<!-- type=system.change {{{ -->\n\t\t\t\t<div ng-switch-when=\"system.change\" class=\"ui-history-system-change\">\n\t\t\t\t\t<div ng-if=\"$ctrl.post.field\">\n\t\t\t\t\t\tChanged\n\t\t\t\t\t\t{{$ctrl.post.field}}\n\t\t\t\t\t\t<em>{{$ctrl.post.from}}</em>\n\t\t\t\t\t\t<i class=\"fa fa-long-arrow-right\"></i>\n\t\t\t\t\t\t<em>{{$ctrl.post.to}}</em>\n\t\t\t\t\t</div>\n\t\t\t\t\t<div ng-if=\"$ctrl.post.fields\">\n\t\t\t\t\t\tChanges:\n\t\t\t\t\t\t<div ng-repeat=\"(field, change) in $ctrl.post.fields track by field\">\n\t\t\t\t\t\t\t{{field}}\n\t\t\t\t\t\t\t<em>{{change.from}}</em>\n\t\t\t\t\t\t\t<i class=\"fa fa-long-arrow-right\"></i>\n\t\t\t\t\t\t\t<em>{{change.to}}</em>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t</div>\n\t\t\t\t</div>\n\t\t\t\t<!-- }}} -->\n\n\t\t\t\t<!-- type=system.status {{{ -->\n\t\t\t\t<div ng-switch-when=\"system.status\" class=\"ui-history-system-status\" ng-bind-html=\"$ctrl.post.body\"></div>\n\t\t\t\t<!-- }}} -->\n\n\t\t\t\t<!-- type unknown {{{ -->\n\t\t\t\t<div ng-switch-default class=\"ui-history-unknown\">\n\t\t\t\t\tUnknown history type: [{{$ctrl.post.type}}]\n\t\t\t\t</div>\n\t\t\t\t<!-- }}} -->\n\t\t\t</div>\n\t\t</div>\n\t", controller: ["$http", "$q", "$sce", "$scope", "$filter", function controller($http, $q, $sce, $scope, $filter) { var $ctrl = this; // Fetcher {{{ $ctrl.posts; $ctrl.post; // Latest post // }}} /** * Load all posts (either via the queryUrl or from the posts array) * @returns {Promise} */ $ctrl.refresh = function () { var force = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : false; if (!force && $ctrl.posts) return $q.resolve(); // User is supplying the post collection rather than us fetching it - do nothing $q.resolve() // Pre loading phase {{{ .then(function () { return $ctrl.isLoading = true; }).then(function () { if (angular.isFunction($ctrl.onLoadingStart)) return $ctrl.onLoadingStart(); }) // }}} // Data fetching - either via queryUrl or examining posts {{{ .then(function () { if (angular.isString($ctrl.queryUrl) || angular.isFunction($ctrl.queryUrl)) { var resolvedUrl = angular.isString($ctrl.queryUrl) ? $ctrl.queryUrl : $ctrl.queryUrl($ctrl); if (!resolvedUrl) throw new Error('Resovled URL is empty'); return $http.get(resolvedUrl).then(function (res) { if (!angular.isArray(res.data)) { throw new Error("Expected history feed at URL \"".concat(resolvedUrl, "\" to be an array but got something else")); } else { return res.data; } })["catch"](function (err) { return console.log('err:', err); }); } else if (angular.isArray($ctrl.posts)) { return $ctrl.posts; } else { throw new Error('Cannot refresh posts - neither queryUrl (func / array) or posts (array) are specified'); } }) // }}} // Misc data mangling {{{ .then(function (data) { $ctrl.posts = data; }) // }}} // If user has a onQuery handler wait for it to mangle / filter the data {{{ .then(function () { if ($ctrl.onQuery) { var res = $ctrl.onQuery({ posts: $ctrl.posts }); if (angular.isArray(res)) $ctrl.posts = res; } }) // }}} // Most recent post .then(function () { $ctrl.posts = $filter('orderBy')($ctrl.posts, 'date', true); $ctrl.post = $ctrl.posts[0]; }) // }}} // Misc data mangling {{{ .then(function (data) { var post = $ctrl.post; if (post && post.body && typeof post.body === 'string' && (post.type == 'user.comment' || post.type == 'user.status' || post.type == 'system.status')) post.body = $sce.trustAsHtml(post.body); }) // }}} // Post loading + catchers {{{ ["catch"](function (error) { if (angular.isFunction($ctrl.onError)) $ctrl.onError({ error: error }); })["finally"](function () { return $ctrl.isLoading = false; })["finally"](function () { if (angular.isFunction($ctrl.onLoadingStop)) return $ctrl.onLoadingStop(); }); // }}} }; // }}} // Init {{{ $ctrl.$onInit = function () { $scope.$watch('$ctrl.queryUrl', function () { return $ctrl.refresh(true); }); }; // }}} }] }) // }}} // uiHistoryAvatar /** * User avatar * @param {Object} user Post author * @param {string} [userAvatar] Template to use as the avatar for the user */ // {{{ .component('userHistoryAvatar', { bindings: { user: '<', userAvatar: '@?', defaultImage: '@?' }, template: "\n\t\t<div ng-if=\"$ctrl.userAvatar\" ng-include=\"$ctrl.userAvatar\"></div>\n\t\t<a ng-if=\"!$ctrl.userAvatar\" ng-href=\"{{$ctrl.user.url}}\" target=\"_blank\">\n\t\t\t<img gravatar-src=\"$ctrl.user.email\" gravatar-size=\"50\" gravatar-default=\"{{$ctrl.defaultImage || 'monsterid'}}\" tooltip=\"{{$ctrl.user.name}}\"/>\n\t\t</a>\n\t" }) // }}} // uiHistoryDate (filter) {{{ /** * Parse a date object into a human readable string * Code chearfully stolen and refactored from https://github.com/samrith-s/relative_date * @see https://github.com/samrith-s/relative_date */ .filter('uiHistoryDate', function () { return function (value) { var date = moment(value); if (!date._isValid) return 'Invalid date'; var diff = moment(Date.now()).diff(date, 'days'); if (diff < 0) { return moment(date).fromNow(true) + ' from now'; } else if (diff < 1) { return date.format('h:mma'); } else if (diff === 1) { return 'Yesterday'; } else if (diff > 1 && diff < 7) { return date.format('dddd'); } else { return date.format('D MMMM, YYYY'); } }; }); // }}}