diff --git a/.jshintrc b/.jshintrc index de8510f32..6ecd915eb 100644 --- a/.jshintrc +++ b/.jshintrc @@ -21,6 +21,10 @@ "unused": "vars", "white": true, "globals": { + // deps that should be removed + "$": false, + "alert": false, + // app deps "angular": false, diff --git a/app/components/content-edit/content-edit-body.html b/app/components/content-edit/content-edit-body.html new file mode 100644 index 000000000..9b60d45f4 --- /dev/null +++ b/app/components/content-edit/content-edit-body.html @@ -0,0 +1,13 @@ + + + + diff --git a/app/components/content-edit/content-edit-body.js b/app/components/content-edit/content-edit-body.js new file mode 100644 index 000000000..865b6cc79 --- /dev/null +++ b/app/components/content-edit/content-edit-body.js @@ -0,0 +1,22 @@ +'use strict'; + +angular.module('bulbs.cms.content.edit.body', [ + 'bulbs.cms.site.config' +]) + .directive('contentEditBody', [ + 'CmsConfig', + function (CmsConfig) { + return { + link: function (scope) { + scope.inlineObjectsPath = CmsConfig.getInlineObjecsPath(); + }, + restrict: 'E', + scope: { + article: '=', + linkDomain: '@', + searchHandler: '@' + }, + templateUrl: CmsConfig.buildComponentPath('content-edit/content-edit-body.html') + }; + } + ]); diff --git a/app/components/content-edit/content-edit-main-image.html b/app/components/content-edit/content-edit-main-image.html new file mode 100644 index 000000000..0aa6dfd20 --- /dev/null +++ b/app/components/content-edit/content-edit-main-image.html @@ -0,0 +1,10 @@ +
+
+ + +
+
diff --git a/app/components/content-edit/content-edit-main-image.js b/app/components/content-edit/content-edit-main-image.js new file mode 100644 index 000000000..bf17be6cc --- /dev/null +++ b/app/components/content-edit/content-edit-main-image.js @@ -0,0 +1,18 @@ +'use strict'; + +angular.module('bulbs.cms.content.edit.mainImage', [ + 'BettyCropper', + 'bulbs.cms.site.config' +]) + .directive('contentEditMainImage', [ + 'CmsConfig', + function (CmsConfig) { + return { + restrict: 'E', + scope: { + article: '=' + }, + templateUrl: CmsConfig.buildComponentPath('content-edit/content-edit-main-image.html') + }; + } + ]); diff --git a/app/components/content-edit/content-edit-title.html b/app/components/content-edit/content-edit-title.html new file mode 100644 index 000000000..107f12c0f --- /dev/null +++ b/app/components/content-edit/content-edit-title.html @@ -0,0 +1,14 @@ + + + + diff --git a/app/components/content-edit/content-edit-title.js b/app/components/content-edit/content-edit-title.js new file mode 100644 index 000000000..db8219162 --- /dev/null +++ b/app/components/content-edit/content-edit-title.js @@ -0,0 +1,17 @@ +'use strict'; + +angular.module('bulbs.cms.content.edit.title', [ + 'bulbs.cms.site.config' +]) + .directive('contentEditTitle', [ + 'CmsConfig', + function (CmsConfig) { + return { + restrict: 'E', + scope: { + article: '=' + }, + templateUrl: CmsConfig.buildComponentPath('content-edit/content-edit-title.html') + }; + } + ]); diff --git a/app/components/content-edit/content-edit.js b/app/components/content-edit/content-edit.js new file mode 100644 index 000000000..1ab667251 --- /dev/null +++ b/app/components/content-edit/content-edit.js @@ -0,0 +1,7 @@ +'use strict'; + +angular.module('bulbs.cms.content', [ + 'bulbs.cms.content.edit.body', + 'bulbs.cms.content.edit.mainImage', + 'bulbs.cms.content.edit.title' +]); diff --git a/app/components/editorial/editor-item.html b/app/components/editorial/editor-item.html new file mode 100644 index 000000000..00e402421 --- /dev/null +++ b/app/components/editorial/editor-item.html @@ -0,0 +1,14 @@ +
+
+
+ {{ item.sender.first_name }} {{ item.sender.last_name }} +
{{ parseCreated(item.created) }}
+
+
{{ parseStatus(item.notes) }}
+
{{ parseNote(item.notes) }}
+
+
diff --git a/app/components/editorial/editor-item.js b/app/components/editorial/editor-item.js new file mode 100644 index 000000000..37e5796c8 --- /dev/null +++ b/app/components/editorial/editor-item.js @@ -0,0 +1,54 @@ +'use strict'; + +angular.module('bulbs.cms.editorial.editorItem', [ + 'bulbs.cms.site.config' +]) + .directive('editorItem', function ($http, EditorItems, moment, CmsConfig) { + return { + restrict: 'E', + templateUrl: CmsConfig.buildComponentPath('editorial/editor-item.html'), + scope: { + article: '=' + }, + link: function (scope, element, attrs) { + + EditorItems.getItems((scope.article.id || '')); + + scope.editorItems = EditorItems; + + scope.parseCreated = function (date) { + return moment(date).format('h:mm A MMM D'); + }; + + /*\ + These two are really, really bad. + + The EditorItem note is in the form of: + + "Status: {{ status_text }}\n\n{{ note_text }}" + + Parse accordingly. + \*/ + + scope.parseNote = function (note) { + var parsed = note.split( + 'Status: ' // Removing 'Status: ' + )[1].split('\n\n'); // Split the note from the status text + + parsed.shift(); // Removing the status_text + + return parsed.join('\n\n'); // In case there are newlines in the note_text + }; + + scope.parseStatus = function (status) { + var parsed = status.split( + 'Status: ' // Removing 'Status: ' + )[1].split( + '\n\n' // Split the note from the status text + ).shift(); // Get the status_text + + return parsed !== 'N/A' ? parsed : ''; + }; + } + }; + }); diff --git a/app/components/editorial/editor-item.less b/app/components/editorial/editor-item.less new file mode 100644 index 000000000..e10b4e27f --- /dev/null +++ b/app/components/editorial/editor-item.less @@ -0,0 +1,41 @@ +editor-item { + > div { + background-color: #f5f5f5; + border: 1px solid #e3e3e3; + margin-right: -200px; + max-height: 500px; + overflow-y: scroll; + position: absolute; + width: 200px; + } + + .note { + border-bottom: 1px solid #e3e3e3; + padding: 0px 10px 10px 15px; + + &:last-child { + border-bottom: 0; + } + + .note-author{ + margin-bottom: 5px; + + div { + color: #a3a3a3; + font-size: 11px; + font-weight: 400; + } + } + + .note-status { + font-size: 13px; + font-weight: 500; + margin-bottom: 13px; + margin-top: 0px; + } + + .note-text { + font-size: 13px; + } + } +} diff --git a/app/components/editorial/editor-items.js b/app/components/editorial/editor-items.js new file mode 100644 index 000000000..2643faa7e --- /dev/null +++ b/app/components/editorial/editor-items.js @@ -0,0 +1,14 @@ +'use strict'; + +angular.module('bulbs.cms.editorial.editorItems', []) + .service('EditorItems', function EditorItems($http) { + this.data = []; + var self = this; + this.getItems = function (article) { + $http.get( + '/cms/api/v1/content/' + article + '/send/' + ).success(function (data, status) { + self.data = data.editor_items; + }).error(function (data, status) {}); + }; + }); diff --git a/app/components/editorial/editorial.js b/app/components/editorial/editorial.js new file mode 100644 index 000000000..9151c74ea --- /dev/null +++ b/app/components/editorial/editorial.js @@ -0,0 +1,4 @@ +angular.module('bulbs.cms.editorial', [ + 'bulbs.cms.editorial.sendToEditorButton', + 'bulbs.cms.editorial.editorItems' +]); diff --git a/app/components/editorial/send-to-editor-button.js b/app/components/editorial/send-to-editor-button.js new file mode 100644 index 000000000..6d4776715 --- /dev/null +++ b/app/components/editorial/send-to-editor-button.js @@ -0,0 +1,33 @@ +'use strict'; + +angular.module('bulbs.cms.editorial.sendToEditorButton', [ + 'bulbs.cms.editorial.sendToEditorModal', + 'bulbs.cms.site.config' +]) + .controller('SendToEditorButtonCtrl', [ + '$scope', '$modal', 'moment', 'CmsConfig', + function ($scope, $modal, moment, CmsConfig) { + $scope.TIMEZONE_LABEL = moment.tz(CmsConfig.getTimezoneName()).format('z'); + + $scope.sendToEditorModal = function (article) { + return $modal.open({ + templateUrl: CmsConfig.buildComponentPath('editorial/send-to-editor-modal.html'), + controller: 'SendtoeditormodalCtrl', + scope: $scope, + resolve: { + article: function(){ return article; } + } + }); + }; + + $scope.getStatus = function (article) { + if(!article || !article.published){ + return 'unpublished'; + }else if(moment(article.published) > moment()){ + return 'scheduled'; + }else{ + return 'published'; + } + }; + } + ]); diff --git a/app/components/editorial/send-to-editor-modal.html b/app/components/editorial/send-to-editor-modal.html new file mode 100644 index 000000000..f8718f63a --- /dev/null +++ b/app/components/editorial/send-to-editor-modal.html @@ -0,0 +1,20 @@ + + + diff --git a/app/components/editorial/send-to-editor-modal.js b/app/components/editorial/send-to-editor-modal.js new file mode 100644 index 000000000..4176e0983 --- /dev/null +++ b/app/components/editorial/send-to-editor-modal.js @@ -0,0 +1,59 @@ +'use strict'; + +angular.module('bulbs.cms.editorial.sendToEditorModal', []) + .controller('SendtoeditormodalCtrl', [ + '$scope', '$http', '$modalInstance', 'EditorItems', 'Login', 'article', + function ($scope, $http, $modalInstance, EditorItems, Login, article) { + // see http://stackoverflow.com/questions/18716113/scope-issue-in-angularjs-using-angularui-bootstrap-modal + $scope.articleStatus = {}; + + $scope.buttonConfig = { + idle: 'Send', + busy: 'Sending', + finished: 'Sent!', + error: 'Error!' + }; + + $scope.articleStatuses = [ + '- Article Status -', + 'Needs First Pass', + 'Needs Final Edit', + 'Needs Copy Edit', + 'Ready to Publish' + ]; + + $scope.sendToEditor = function (article) { + var status, note, noteToEditor; + + noteToEditor = $scope.noteToEditor || 'No note attached'; + + // Leaving most of the hacks here in the temporary code + // until we figure out a more permanent solution + if ($scope.articleStatus.s !== $scope.articleStatuses[0]) { + status = $scope.articleStatus.s; + note = 'Status: ' + $scope.articleStatus.s + '\n\n' + noteToEditor; + } else { + status = ''; + note = 'Status: N/A' + '\n\n' + noteToEditor; + } + + return $http({ + url: '/cms/api/v1/content/' + article.id + '/send/', + method: 'POST', + data: { + notes: note, + status: status + } + }).success(function (data) { + EditorItems.getItems(article.id); + $scope.publishSuccessCbk({article: article, response: data}); + $modalInstance.close(); + }).error(function (error, status, data) { + if(status === 403) { + Login.showLoginModal(); + } + $modalInstance.dismiss(); + }); + }; + } + ]); diff --git a/app/components/editorial/send-to-editor.less b/app/components/editorial/send-to-editor.less new file mode 100644 index 000000000..1acfd7d83 --- /dev/null +++ b/app/components/editorial/send-to-editor.less @@ -0,0 +1,4 @@ +.modal-body .article-statuses { + width: 200px; + margin-bottom: 35px; +} diff --git a/app/components/status-filter/status-filter-directive.js b/app/components/status-filter/status-filter-directive.js index c5010e218..7d0d2030b 100644 --- a/app/components/status-filter/status-filter-directive.js +++ b/app/components/status-filter/status-filter-directive.js @@ -4,35 +4,21 @@ angular.module('statusFilter.directive', [ 'bulbs.cms.site.config', 'contentServices.listService' ]) - .provider('StatusFilterOptions', function (moment) { - var _statuses = [ - {label: 'Draft', key: 'status', value: 'draft'}, - {label: 'Published', key: 'before', value: function () { return moment().format('YYYY-MM-DDTHH:mmZ'); }}, - {label: 'Scheduled', key: 'after', value: function () { return moment().format('YYYY-MM-DDTHH:mmZ'); }}, - {label: 'All', key: null, value: null} - ]; - - this.setStatuses = function (statuses) { - _statuses = statuses; - }; - - this.$get = function () { - return { - getStatuses: function () { - return _statuses; - } - }; - }; - - }) - .directive('statusFilter', function ($location, _, StatusFilterOptions, ContentListService, CmsConfig) { + .directive('statusFilter', function ($location, _, ContentListService, CmsConfig) { return { templateUrl: CmsConfig.buildComponentPath('status-filter/status-filter.html'), restrict: 'E', scope: {}, controller: 'ContentlistCtrl', link: function postLink(scope, element, attrs) { - scope.options = StatusFilterOptions.getStatuses(); + var dateFormat = 'YYYY-MM-DDTHH:mmZ'; + scope.options = [ + { label: 'Draft', key: 'status', value: 'Draft'}, + { label: 'Edit', key: 'status', value: 'Waiting for Editor'}, + { label: 'Scheduled', key: 'after', value: function () { return moment().format(dateFormat); } }, + { label: 'Published', key: 'before', value: function () { return moment().format(dateFormat); } }, + { label: 'All', key: null, value: null } + ]; /** * Test if a particular option is currently active by comparing it to diff --git a/app/components/video/video-search.html b/app/components/video/video-search.html new file mode 100644 index 000000000..ac7c6d621 --- /dev/null +++ b/app/components/video/video-search.html @@ -0,0 +1,12 @@ + + + diff --git a/app/components/video/video-search.js b/app/components/video/video-search.js new file mode 100644 index 000000000..171a0b1f1 --- /dev/null +++ b/app/components/video/video-search.js @@ -0,0 +1,59 @@ +'use strict'; + +angular.module('bulbs.cms.video.videoSearch', [ + 'bulbs.cms.site.config', + 'lodash', + 'VideohubClient.api', + 'VideohubClient.settings', + 'uuid4' +]) + .directive('videoSearch', [ + '_', 'CmsConfig', 'uuid4', + function (_, CmsConfig, uuid4) { + return { + restrict: 'E', + require: 'ngModel', + templateUrl: CmsConfig.buildComponentPath('video/video-search.html'), + scope: { + label: '@videoSearchLabel', + onSelect: '&videoSearchLabel' + }, + controller: [ + '$scope', 'Video', 'VIDEOHUB_DEFAULT_CHANNEL', + function ($scope, Video, VIDEOHUB_DEFAULT_CHANNEL) { + $scope.videoChannel = VIDEOHUB_DEFAULT_CHANNEL; + + $scope.itemDisplayFormatter = function (video) { + if (_.isObject(video)) { + return video.id + ' - ' + video.title; + } + }; + + $scope.searchVideos = function (query) { + return Video.$postSearch({ + query: query, + channel: VIDEOHUB_DEFAULT_CHANNEL + }); + }; + } + ], + link: function (scope, elements, attrs, ngModelCtrl) { + scope.uuid = uuid4.generate(); + + if (ngModelCtrl) { + scope.ngModel = ngModelCtrl; + + ngModelCtrl.$render = function () { + if (_.has(ngModelCtrl.$modelValue, 'title') && !scope.initialValue) { + scope.initialValue = scope.itemDisplayFormatter(ngModelCtrl.$modelValue); + } + }; + + scope.onSelect = function (selection) { + ngModelCtrl.$commitViewValue(); + }; + } + } + }; + } + ]); diff --git a/app/components/video/video.js b/app/components/video/video.js new file mode 100644 index 000000000..b3aa0fc3a --- /dev/null +++ b/app/components/video/video.js @@ -0,0 +1 @@ +angular.module('bulbs.cms.video', ['bulbs.cms.video.videoSearch']); diff --git a/app/index.html b/app/index.html index 853c683c6..88785cc50 100644 --- a/app/index.html +++ b/app/index.html @@ -37,6 +37,7 @@ + @@ -52,16 +53,8 @@ - - - -
-
You can't see this!
- - @@ -118,6 +111,7 @@ + @@ -141,6 +135,11 @@ + + + + + @@ -149,10 +148,14 @@ - + + + + + @@ -211,6 +214,8 @@ + + @@ -243,7 +248,6 @@ - @@ -342,6 +346,7 @@ .setFirebaseUrl('https://luminous-fire-8340.firebaseio.com/') .setImageApiUrl('http://clickholeimg.local') .setImageApiKey('123abc') + .setInlineObjectsPath('/views/inline-objects.json') .setInternalUrl('onion.local') .setNavLogoPath('/images/onion-logo.png') .setVideoPath('videos/embed?id=') @@ -359,6 +364,14 @@ resultsElement.html("Searching for: " + term + "
Fart
Butt
Dumb
"); }; + angular.module('bulbsCmsAppDev', ['bulbsCmsApp.mockApi', 'bulbsCmsApp', 'bulbs.api.mock']); + + // this has something to do with angular routing + // forcing it to no be a single-page app + $("body").on("click", "a", function(e){ + if($(this).attr("target") == "_blank") return true; + $(this).attr("target", "_self"); + }); @@ -375,24 +388,6 @@ - - diff --git a/app/scripts/app.js b/app/scripts/app.js index afc949893..f66b8eb8e 100644 --- a/app/scripts/app.js +++ b/app/scripts/app.js @@ -15,6 +15,8 @@ angular.module('OnionEditor', []).constant('OnionEditor', window.OnionEditor); angular.module('bulbsCmsApp', [ 'bulbs.cms.site.config', + 'bulbs.cms.editorial', + 'bulbs.cms.content', 'ngCookies', 'ngResource', @@ -34,10 +36,10 @@ angular.module('bulbsCmsApp', [ 'ipCookie', 'bulbs.api', 'OnionEditor', - // shared + 'contentServices', 'cms.tunic', - // components + 'bettyEditable', 'bugReporter', 'campaigns', @@ -46,7 +48,6 @@ angular.module('bulbsCmsApp', [ 'filterListWidget', 'polls', 'promotedContent', - 'statusFilter', 'templateTypeField', 'specialCoverage', 'sections', diff --git a/app/scripts/directives/add-image.js b/app/scripts/directives/add-image.js deleted file mode 100644 index 1e57a8576..000000000 --- a/app/scripts/directives/add-image.js +++ /dev/null @@ -1,45 +0,0 @@ -'use strict'; - -angular.module('bulbsCmsApp') - .directive('addImage', function ($http, $window) { - return { - restrict: 'E', - templateUrl: '/views/add-image.html', - scope: { - article: '=' - }, - link: function (scope, element, attrs) { - var attrName = attrs.attrName || 'images'; - scope.article[attrName] = scope.article[attrName] || []; - - if (attrs.caption === 'false') { scope.hideCaption = true; } - scope.format = attrs.format || 'jpg'; - scope.crop = attrs.crop || '16x9'; - scope.placeholderText = attrs.placeholderText || 'Optional Image'; - - scope.addAnImage = function () { - $window.uploadImage({ - onSuccess: function (data) { - scope.$apply(function () { - scope.article[attrName].push({ - id: data.id.toString(), - alt: null, - caption: null - }); - setTimeout($window.picturefill, 200); - }); - }, - onError: function (data) { - scope.$apply(function () { - $window.alert('Error: ', data); - }); - }, - onProgress: function (data) { - - } - }); - }; - } - - }; - }); diff --git a/app/scripts/libraries/upload-video.js b/app/scripts/libraries/upload-video.js new file mode 100644 index 000000000..4fd61a3fb --- /dev/null +++ b/app/scripts/libraries/upload-video.js @@ -0,0 +1,313 @@ +!(function(window){ + 'use strict'; + //TODO: get from VIDEO ENCODING settings via VideoAttrs + + var POSTER_THUMBNAIL_COUNT = 20; + var PUBLIC_URL = 'http://v.theonion.com/avclubmedia/'; + //append hidden file input + $('#s3upload-file-input').remove(); + $('body').append(''); + + //append video edit modal + $('#video-edit-modal').remove(); + $('body').append($($('#video-edit-modal-template').html())); //template lives in jstemplates.html + + //save button listener + $('#video-edit-modal').find('.save-button').on('click', function(e){ + var newData = $('#video-edit-modal')[0].videoData; + newData['poster_url'] = $('#video-edit-modal').find('input[name="poster_url"]').val(); + console.log('saving video. new data:', newData); + saveVideo(newData, { + onSuccess: function(data){ + console.log('save success'); + $('[data-videoid="' + data.id + '"]').find('iframe')[0].contentWindow.location.reload(); + }, + onError: function(data){ + console.log('save error', data); + } + }); + }); + + $('#video-edit-modal').find('.reencode-button').on('click', function(e){ + console.log('reencode click'); + var data = $('#video-edit-modal')[0].videoData; + encode(data.id); + }); + + $('#video-edit-modal .next-thumb').click(function(e) { + //increment video thumb + var index = getPosterIndex() + 1; + if (POSTER_THUMBNAIL_COUNT - 1 < index) { + index = 0; + } + setPosterUrl(posterUrl(index)); + }); + + $('#video-edit-modal .prev-thumb').click(function(e) { + //decrement video thumb + var index = getPosterIndex() -1; + if (index < 0) { + index = POSTER_THUMBNAIL_COUNT - 1; + } + setPosterUrl(posterUrl(index)); + }); + + $('#video-edit-modal input[name="poster_url"]') + .bind('change', updatePosterPreview) + .bind('keyup', updatePosterPreview); + + function setPosterUrl(url) { + $('#video-edit-modal input[name="poster_url"]').val(url); + updatePosterPreview(); + } + + function getPosterIndex() { + var url = $('#video-edit-modal input[name="poster_url"]').val(); + var match = url.match(/frame_(\d{4})\.jpg/); + if (match) { + console.log('match'); + return parseInt(match[1]); } + else { + return -1; + } + } + + function posterUrl(index) { + console.log($('#video-edit-modal')[0].videoData); + var id = $('#video-edit-modal')[0].videoData.id; + return PUBLIC_URL + 'video/' + id + '/frame_' + ('000'+ index).slice(-4) + '.jpg'; + } + + function updatePosterPreview() { + var posterUrl = $('#video-edit-modal input[name="poster_url"]').val(); + $('#video-edit-modal .poster-preview').attr('src', posterUrl); + } + + function uploadVideo(element, callbacks) { + console.log('upload video here'); + var input = $('#s3upload-file-input'); //make this more 'angular' + input.click(); + input.unbind('change'); + input.bind('change', function(e) { + doVideoUpload(element, callbacks); + }); + } + + window.uploadVideo = uploadVideo; + + function doVideoUpload(element, callbacks) { + console.log('doVideoUpload'); + var fileInput = $('#s3upload-file-input')[0]; + var file; + + if (fileInput.files.length !== 0) { + file = fileInput.files[0]; + // We have a file upload limit of 1024MB + if (file.size > (1024 * 1024 * 1024)) { + alert('Upload file cannot be larger than 1024MB.'); + return; + } + + if (file.type.indexOf('video/') !== 0) { + alert('You must upload a video file.'); + return; + } + + } else { + return; + } + + console.log('posting'); + + var onError = function () {}; + var onSuccess = function () {}; + + if (callbacks) { + if (callbacks.onSuccess) { + onSuccess = callbacks.onSuccess; + } + if (callbacks.onError) { + onError = callbacks.onError; + } + } + + $.ajax({ + type: 'POST', + url: '/videos/api/video/', + data: {'name': file.name}, + success: function(data){ + onSuccess(data.id); + $(element).attr('data-videoid', data.id); + initVideoWidget(data.id); + upload(file, data.id, element, callbacks); + }, + error: function(data, status, error){ + console.log('error', data, status, error); + if (data.status === 403) { + window.showLoginModal(); + } + onError(data); + } + }); } + + function upload(file, videoid, element, callbacks){ + var url = 'https://' + window.videoAttrs.bucket + '.s3.amazonaws.com'; + var formData = new FormData(); + var path = window.videoAttrs.directory + '/' + videoid + '/original'; + + formData.append('key', path); + formData.append('AWSAccessKeyId', window.videoAttrs.AWSAccessKeyId); + formData.append('acl', window.videoAttrs.acl); + formData.append('success_action_status', window.videoAttrs.success_action_status); + formData.append('policy', window.videoAttrs.policy); + formData.append('signature', window.videoAttrs.signature); + formData.append('file', file); + + $(element).append($($('#video-upload-status-overlay').html())); + $(element).show(); + $(element).find('.upload-progress').show(); + callbacks.onProgress && callbacks.onProgress(0); // jshint ignore:line + + // TODO: set it so user cant leave page right now + + $.ajax(url, { + processData: false, + contentType: false, + data: formData, + type: 'POST', + xhr: function() { + var req = $.ajaxSettings.xhr(); + if (req) { + req.upload.addEventListener('progress', function(e) { + var percent = (e.loaded / e.total ) * 100; + $(element).find('.upload-progress .progress-bar').width(percent + '%'); + }, false); + } + + return req; + }, + + success: function(data) { + $(element).find('.video-status-container').remove(); + // TODO: user can leave page now + encode(videoid, callbacks); + }, + + error: function(data){ + console.log('upload ERROR', data); + callbacks.onError && callbacks.onError(); // jshint ignore:line + } + }); + } + + function encode(videoid){ + // Kick off the zencoder job + console.log('encoding'); + $.ajax({ + url: '/videos/api/video/' + videoid + '/encode/', + method: 'POST', + + success: function(data){ + console.log('encode success', data); + var jobid = data.id; + appendEncodeProgress(videoid, jobid); + }, + + error: function(){ + console.log('encode error'); + } + }); + } + + function initVideoWidget(id){ + console.log('init video widget'); + //get video data + $.get('/videos/api/video/' + id + '/', function(data){ + bindEditEvents(data); + var status = data.status; + if (status === 'In Progress') { + appendEncodeProgress(id, data.job_id); + } + }); + } + + window.initVideoWidget = initVideoWidget; + + function bindEditEvents(data){ + var container = $('[data-videoid="' + data.id + '"]'); + container.css('position', 'relative'); + + //show/hide button listeners + container.find('.edit-button').on('click', function(e) { + $('#video-edit-modal').find('.video-name').text(data.name); + $('#video-edit-modal').find('input[name="poster_url"]').val(data.poster_url); + $('#video-edit-modal')[0].videoData = data; + $('#video-edit-modal .poster-preview').attr('src', data.poster_url); + $('#video-edit-modal').modal('show'); + }); + } + + function editVideo(id){ + $.get('/videos/api/video/' + id + '/', function(data){ + $('#video-edit-modal').find('h3.video-name').text(data.name); + $('#video-edit-modal').find('input[name="poster_url"]').val(data.poster_url); + $('#video-edit-modal .poster-preview').attr('src', data.poster_url); + $('#video-edit-modal')[0].videoData = data; + $('#video-edit-modal').modal('show'); + }); + } + + window.editVideo = editVideo; + + function saveVideo(data, callbacks){ + console.log('save video here', data); + $.ajax('/videos/api/video/' + data.id + '/', + { + type: 'PUT', + contentType: 'application/json; charset=utf-8', + data: JSON.stringify(data), + processData: false, + success: callbacks.onSuccess, + error: callbacks.onError + } + ); + } + + function appendEncodeProgress(videoid, jobid){ + console.log('append encode progress'); + var container = $('[data-videoid="' + videoid + '"]'); + container.find('.video-status-container').remove(); + container.append($('#video-encode-status-overlay').html()); + container.find('.video-edit-container').addClass('encoding'); + + updateEncodeProgress(jobid, container); + } + + function updateEncodeProgress(jobid, container){ + $.ajax('https://app.zencoder.com/api/v2/jobs/' + jobid + '/progress.json', { + type: 'GET', + data: { + api_key: window.videoAttrs.zencoderApiKey + }, + + success: function(data) { + if (data.state === 'waiting' || data.state === 'pending' || data.state === 'processing') { + if (data.progress > 5) { + $(container).find('.progress-bar').width(data.progress + '%'); + setTimeout(function(){ updateEncodeProgress(jobid, container); }, 2000); + } else { + setTimeout(function(){ updateEncodeProgress(jobid, container); }, 2000); + } + } else { + encodingWidgetCleanup(container); + } + } + }); + } + + function encodingWidgetCleanup(container){ + container.find('.video-status-container').remove(); + container.find('.video-edit-container').removeClass('encoding'); + container.find('iframe')[0].contentWindow.location.reload(); + } +}(window)); // jshint ignore:line diff --git a/app/shared/cms-config/cms-config-base.js b/app/shared/cms-config/cms-config-base.js index 8ee261666..44f6ce363 100644 --- a/app/shared/cms-config/cms-config-base.js +++ b/app/shared/cms-config/cms-config-base.js @@ -3,7 +3,8 @@ angular.module('bulbs.cms.base.config', [ 'bulbs.cms.config', 'bulbs.cms.customSearch.config', - 'ngClipboard' + 'ngClipboard', + 'statusFilter' ]) .config([ 'CmsConfigProvider', 'CustomSearchConfigProvider', 'ngClipProvider', diff --git a/app/shared/cms-config/cms-config.js b/app/shared/cms-config/cms-config.js index 8b83d86f0..e1c57334e 100644 --- a/app/shared/cms-config/cms-config.js +++ b/app/shared/cms-config/cms-config.js @@ -52,6 +52,8 @@ angular.module('bulbs.cms.config', [ var imageApiKey = ''; // url for internal links, those that are not accessible to the public var internalUrl = ''; + // path to inline editor buttons configuration + var inlineObjectsPath = ''; // path to cms logo static asset var navLogoPath = ''; // path to shared directory @@ -140,6 +142,14 @@ angular.module('bulbs.cms.config', [ return this; }; + this.setInlineObjectsPath = function (value) { + inlineObjectsPath = checkOrError( + value, _.isString, + 'inline objects path must be a string!' + ); + return this; + }; + this.setImageApiUrl = function (value) { imageApiUrl = checkOrError( value, _.isString, @@ -282,6 +292,7 @@ angular.module('bulbs.cms.config', [ getCmsName: _.constant(cmsName), getFirebaseMaxArticleHistory: _.constant(firebaseMaxArticleHistory), getImageApiKey: _.constant(imageApiKey), + getInlineObjecsPath: _.constant(inlineObjectsPath), getNavLogoPath: _.constant(navLogoPath), getTimezoneName: _.constant(timezoneName), getTopBarMapping: function (name) { diff --git a/app/shared/cms-config/cms-config.tests.js b/app/shared/cms-config/cms-config.tests.js index 1dce390d6..6c9e1dbae 100644 --- a/app/shared/cms-config/cms-config.tests.js +++ b/app/shared/cms-config/cms-config.tests.js @@ -275,6 +275,33 @@ describe('CmsConfig', function () { }); }); + context('inline objects path', function () { + + it('should provide a getter and setter', function () { + var path = '/inline-objects.json'; + + configs.setInlineObjectsPath(path); + + expect(sealedConfigs().getInlineObjecsPath()).to.equal(path); + }); + + it('should throw an error if given value is not a string', function () { + + expect(function () { + configs.setInlineObjectsPath(123); + }).to.throw( + BulbsCmsConfigError, + 'Configuration Error (CmsConfig): inline objects path must be a string!' + ); + }); + + it('should return config object', function () { + + expect(configs.setInlineObjectsPath('/inline-objects.json')) + .to.equal(configs); + }); + }); + context('internal url', function () { it('should provide a setter and getter', function () { diff --git a/app/styles/_components.less b/app/styles/_components.less index 37aaf9a77..b681bec0e 100644 --- a/app/styles/_components.less +++ b/app/styles/_components.less @@ -1,18 +1,20 @@ @import "_mixin"; // injector:less_components -@import "../components/filter-widget/filter-widget.less"; +@import "../components/filter-list-widget/filter-list-widget.less"; @import "../components/promoted-content/promoted-content.less"; -@import "../components/polls/polls-edit/polls-edit.less"; +@import "../components/filter-widget/filter-widget.less"; @import "../components/campaigns/campaigns-edit/campaigns-edit-sponsor-pixel/campaigns-edit-sponsor-pixel.less"; @import "../components/custom-search/custom-search-content-item/custom-search-content-item.less"; @import "../components/custom-search/custom-search-group/custom-search-group-condition/custom-search-group-condition.less"; @import "../components/custom-search/custom-search-group/custom-search-group.less"; @import "../components/custom-search/custom-search-simple-content-search/custom-search-simple-content-search.less"; @import "../components/custom-search/custom-search.less"; -@import "../components/filter-list-widget/filter-list-widget.less"; +@import "../components/editorial/editor-item.less"; +@import "../components/editorial/send-to-editor.less"; @import "../components/promoted-content/promoted-content-time-picker/promoted-content-time-picker.less"; @import "../components/campaigns/campaigns-edit/campaigns-edit.less"; +@import "../components/polls/polls-edit/polls-edit.less"; @import "../components/polls/polls-list/polls-list.less"; @import "../components/promoted-content/promoted-content-article/promoted-content-article.less"; @import "../components/promoted-content/promoted-content-list/promoted-content-list.less"; diff --git a/app/styles/edit-page.less b/app/styles/edit-page.less index c00d00109..7c5bcb4a1 100644 --- a/app/styles/edit-page.less +++ b/app/styles/edit-page.less @@ -90,4 +90,4 @@ template-type-field { label { font-size: 12px; } -} \ No newline at end of file +} diff --git a/app/styles/upload-video.less b/app/styles/upload-video.less new file mode 100644 index 000000000..b021203ee --- /dev/null +++ b/app/styles/upload-video.less @@ -0,0 +1,78 @@ +.video-edit-container{ + position: absolute; + top: 0px; + width: 100%; + height: 0px; + + .video-edit-overlay{ + width: 100%; + height: 100%; + background-color: rgba(100,100,100,0.5); + color: white; + padding: 15px; + display: none; + input{ width: 100%; } + } + + .edit-button, .clear-button{ + position: absolute; + top: 15px; + left: 15px; + } +} + +.video-edit-container.editing{ + height: 100%; + .video-edit-overlay{ display: block; } + .edit-button{ display: none; } +} + +.video-edit-container.encoding{ + .reencode-button{ display: none; } +} + +.video-status-container{ + position: absolute !important; + bottom: 0px !important; + width: 100% !important; + height: auto !important; + padding-bottom: 34px !important; +} + +.s3uploadButton { + border-top-left-radius: 0; + border-bottom-left-radius: 0; + margin-top: -34px; /* This is a little hack. */ + z-index: 15; + position: relative; +} + +.s3uploadProgress { + height: 34px !important; + z-index: 10; + position: absolute; + bottom: 0px; + margin-bottom: 0px; + width: 100%; + background-color: #eee; +} + +.s3uploadProgress .progress-bar { + border-top-left-radius: 4px; + border-bottom-left-radius: 4px; + z-index: 10; + position: relative; +} + +.s3uploadProgress .progress-bar span { + line-height: 34px; + padding-left: 20px; + font-size: 1em; + font-weight: 500; + color: white; + text-shadow: + -1px -1px 0 #000, + 1px -1px 0 #000, + -1px 1px 0 #000, + 1px 1px 0 #000; +}