Generalize file uploader.
Previously the file upload dialog was PDF- and textbook-specific. The changes are adding parameters to the FileUpload model for the file type, and adding an onSuccess callback to the UploadDialog view. Also moved upload-specific SASS into its own file.
This commit is contained in:
@@ -244,6 +244,7 @@ PIPELINE_JS = {
|
||||
'js/models/course.js',
|
||||
'js/models/section.js', 'js/views/section.js',
|
||||
'js/models/metadata_model.js', 'js/views/metadata_editor_view.js',
|
||||
'js/models/uploads.js', 'js/views/uploads.js',
|
||||
'js/models/textbook.js', 'js/views/textbook.js',
|
||||
'js/views/assets.js', 'js/utility.js'],
|
||||
'output_filename': 'js/cms-application.js',
|
||||
|
||||
@@ -301,7 +301,7 @@ describe "CMS.Views.EditChapter", ->
|
||||
@view.render().$(".action-upload").click()
|
||||
ctorOptions = uploadSpies.constructor.mostRecentCall.args[0]
|
||||
expect(ctorOptions.model.get('title')).toMatch(/abcde/)
|
||||
expect(ctorOptions.chapter).toBe(@model)
|
||||
expect(typeof ctorOptions.onSuccess).toBe('function')
|
||||
expect(uploadSpies.show).toHaveBeenCalled()
|
||||
|
||||
it "saves content when opening upload dialog", ->
|
||||
@@ -323,7 +323,15 @@ describe "CMS.Views.UploadDialog", ->
|
||||
|
||||
@model = new CMS.Models.FileUpload()
|
||||
@chapter = new CMS.Models.Chapter()
|
||||
@view = new CMS.Views.UploadDialog({model: @model, chapter: @chapter})
|
||||
@view = new CMS.Views.UploadDialog(
|
||||
model: @model
|
||||
onSuccess: (response) =>
|
||||
options = {}
|
||||
if !@chapter.get('name')
|
||||
options.name = response.displayname
|
||||
options.asset_path = response.url
|
||||
@chapter.set(options)
|
||||
)
|
||||
spyOn(@view, 'remove').andCallThrough()
|
||||
|
||||
# create mock file input, so that we aren't subject to browser restrictions
|
||||
|
||||
@@ -155,24 +155,4 @@ CMS.Collections.ChapterSet = Backbone.Collection.extend({
|
||||
return this.length === 0 || this.every(function(m) { return m.isEmpty(); });
|
||||
}
|
||||
});
|
||||
CMS.Models.FileUpload = Backbone.Model.extend({
|
||||
defaults: {
|
||||
"title": "",
|
||||
"message": "",
|
||||
"selectedFile": null,
|
||||
"uploading": false,
|
||||
"uploadedBytes": 0,
|
||||
"totalBytes": 0,
|
||||
"finished": false
|
||||
},
|
||||
// NOTE: validation functions should return non-internationalized error
|
||||
// messages. The messages will be passed through gettext in the template.
|
||||
validate: function(attrs, options) {
|
||||
if(attrs.selectedFile && attrs.selectedFile.type !== "application/pdf") {
|
||||
return {
|
||||
message: "Only PDF files can be uploaded. Please select a file ending in .pdf to upload.",
|
||||
attributes: {selectedFile: true}
|
||||
};
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
23
cms/static/js/models/uploads.js
Normal file
23
cms/static/js/models/uploads.js
Normal file
@@ -0,0 +1,23 @@
|
||||
CMS.Models.FileUpload = Backbone.Model.extend({
|
||||
defaults: {
|
||||
"title": "",
|
||||
"message": "",
|
||||
"selectedFile": null,
|
||||
"uploading": false,
|
||||
"uploadedBytes": 0,
|
||||
"totalBytes": 0,
|
||||
"finished": false,
|
||||
"mimeType": "application/pdf",
|
||||
"fileType": "PDF"
|
||||
},
|
||||
// NOTE: validation functions should return non-internationalized error
|
||||
// messages. The messages will be passed through gettext in the template.
|
||||
validate: function(attrs, options) {
|
||||
if(attrs.selectedFile && attrs.selectedFile.type !== this.attributes.mimeType) {
|
||||
return {
|
||||
message: "Only " + this.attributes.fileType + " files can be uploaded. Please select a file ending in ." + this.attributes.fileType.toLowerCase() + " to upload.",
|
||||
attributes: {selectedFile: true}
|
||||
};
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -245,118 +245,18 @@ CMS.Views.EditChapter = Backbone.View.extend({
|
||||
{name: section.escape('name')}),
|
||||
message: "Files must be in PDF format."
|
||||
});
|
||||
var view = new CMS.Views.UploadDialog({model: msg, chapter: this.model});
|
||||
var that = this;
|
||||
var view = new CMS.Views.UploadDialog({
|
||||
model: msg,
|
||||
onSuccess: function(response) {
|
||||
var options = {};
|
||||
if(!that.model.get('name')) {
|
||||
options.name = response.displayname;
|
||||
}
|
||||
options.asset_path = response.url;
|
||||
that.model.set(options);
|
||||
},
|
||||
});
|
||||
$(".wrapper-view").after(view.show().el);
|
||||
}
|
||||
});
|
||||
|
||||
CMS.Views.UploadDialog = Backbone.View.extend({
|
||||
options: {
|
||||
shown: true,
|
||||
successMessageTimeout: 2000 // 2 seconds
|
||||
},
|
||||
initialize: function() {
|
||||
this.template = _.template($("#upload-dialog-tpl").text());
|
||||
this.listenTo(this.model, "change", this.render);
|
||||
},
|
||||
render: function() {
|
||||
var isValid = this.model.isValid();
|
||||
var selectedFile = this.model.get('selectedFile');
|
||||
var oldInput = this.$("input[type=file]").get(0);
|
||||
this.$el.html(this.template({
|
||||
shown: this.options.shown,
|
||||
url: CMS.URL.UPLOAD_ASSET,
|
||||
title: this.model.escape('title'),
|
||||
message: this.model.escape('message'),
|
||||
selectedFile: selectedFile,
|
||||
uploading: this.model.get('uploading'),
|
||||
uploadedBytes: this.model.get('uploadedBytes'),
|
||||
totalBytes: this.model.get('totalBytes'),
|
||||
finished: this.model.get('finished'),
|
||||
error: this.model.validationError
|
||||
}));
|
||||
// Ideally, we'd like to tell the browser to pre-populate the
|
||||
// <input type="file"> with the selectedFile if we have one -- but
|
||||
// browser security prohibits that. So instead, we'll swap out the
|
||||
// new input (that has no file selected) with the old input (that
|
||||
// already has the selectedFile selected). However, we only want to do
|
||||
// this if the selected file is valid: if it isn't, we want to render
|
||||
// a blank input to prompt the user to upload a different (valid) file.
|
||||
if (selectedFile && isValid) {
|
||||
$(oldInput).removeClass("error");
|
||||
this.$('input[type=file]').replaceWith(oldInput);
|
||||
}
|
||||
return this;
|
||||
},
|
||||
events: {
|
||||
"change input[type=file]": "selectFile",
|
||||
"click .action-cancel": "hideAndRemove",
|
||||
"click .action-upload": "upload"
|
||||
},
|
||||
selectFile: function(e) {
|
||||
this.model.set({
|
||||
selectedFile: e.target.files[0] || null
|
||||
});
|
||||
},
|
||||
show: function(e) {
|
||||
if(e && e.preventDefault) { e.preventDefault(); }
|
||||
this.options.shown = true;
|
||||
$body.addClass('dialog-is-shown');
|
||||
return this.render();
|
||||
},
|
||||
hide: function(e) {
|
||||
if(e && e.preventDefault) { e.preventDefault(); }
|
||||
this.options.shown = false;
|
||||
$body.removeClass('dialog-is-shown');
|
||||
return this.render();
|
||||
},
|
||||
hideAndRemove: function(e) {
|
||||
if(e && e.preventDefault) { e.preventDefault(); }
|
||||
return this.hide().remove();
|
||||
},
|
||||
upload: function(e) {
|
||||
this.model.set('uploading', true);
|
||||
this.$("form").ajaxSubmit({
|
||||
success: _.bind(this.success, this),
|
||||
error: _.bind(this.error, this),
|
||||
uploadProgress: _.bind(this.progress, this),
|
||||
data: {
|
||||
// don't show the generic error notification; we're in a modal,
|
||||
// and we're better off modifying it instead.
|
||||
notifyOnError: false
|
||||
}
|
||||
});
|
||||
},
|
||||
progress: function(event, position, total, percentComplete) {
|
||||
this.model.set({
|
||||
"uploadedBytes": position,
|
||||
"totalBytes": total
|
||||
});
|
||||
},
|
||||
success: function(response, statusText, xhr, form) {
|
||||
this.model.set({
|
||||
uploading: false,
|
||||
finished: true
|
||||
});
|
||||
var chapter = this.options.chapter;
|
||||
if(chapter) {
|
||||
var options = {};
|
||||
if(!chapter.get("name")) {
|
||||
options.name = response.displayname;
|
||||
}
|
||||
options.asset_path = response.url;
|
||||
chapter.set(options);
|
||||
}
|
||||
var that = this;
|
||||
this.removalTimeout = setTimeout(function() {
|
||||
that.hide().remove();
|
||||
}, this.options.successMessageTimeout);
|
||||
},
|
||||
error: function() {
|
||||
this.model.set({
|
||||
"uploading": false,
|
||||
"uploadedBytes": 0,
|
||||
"title": gettext("We're sorry, there was an error")
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
105
cms/static/js/views/uploads.js
Normal file
105
cms/static/js/views/uploads.js
Normal file
@@ -0,0 +1,105 @@
|
||||
CMS.Views.UploadDialog = Backbone.View.extend({
|
||||
options: {
|
||||
shown: true,
|
||||
successMessageTimeout: 2000 // 2 seconds
|
||||
},
|
||||
initialize: function() {
|
||||
this.template = _.template($("#upload-dialog-tpl").text());
|
||||
this.listenTo(this.model, "change", this.render);
|
||||
},
|
||||
render: function() {
|
||||
var isValid = this.model.isValid();
|
||||
var selectedFile = this.model.get('selectedFile');
|
||||
var oldInput = this.$("input[type=file]").get(0);
|
||||
this.$el.html(this.template({
|
||||
shown: this.options.shown,
|
||||
url: CMS.URL.UPLOAD_ASSET,
|
||||
title: this.model.escape('title'),
|
||||
message: this.model.escape('message'),
|
||||
selectedFile: selectedFile,
|
||||
uploading: this.model.get('uploading'),
|
||||
uploadedBytes: this.model.get('uploadedBytes'),
|
||||
totalBytes: this.model.get('totalBytes'),
|
||||
finished: this.model.get('finished'),
|
||||
error: this.model.validationError
|
||||
}));
|
||||
// Ideally, we'd like to tell the browser to pre-populate the
|
||||
// <input type="file"> with the selectedFile if we have one -- but
|
||||
// browser security prohibits that. So instead, we'll swap out the
|
||||
// new input (that has no file selected) with the old input (that
|
||||
// already has the selectedFile selected). However, we only want to do
|
||||
// this if the selected file is valid: if it isn't, we want to render
|
||||
// a blank input to prompt the user to upload a different (valid) file.
|
||||
if (selectedFile && isValid) {
|
||||
$(oldInput).removeClass("error");
|
||||
this.$('input[type=file]').replaceWith(oldInput);
|
||||
}
|
||||
return this;
|
||||
},
|
||||
events: {
|
||||
"change input[type=file]": "selectFile",
|
||||
"click .action-cancel": "hideAndRemove",
|
||||
"click .action-upload": "upload"
|
||||
},
|
||||
selectFile: function(e) {
|
||||
this.model.set({
|
||||
selectedFile: e.target.files[0] || null
|
||||
});
|
||||
},
|
||||
show: function(e) {
|
||||
if(e && e.preventDefault) { e.preventDefault(); }
|
||||
this.options.shown = true;
|
||||
$body.addClass('dialog-is-shown');
|
||||
return this.render();
|
||||
},
|
||||
hide: function(e) {
|
||||
if(e && e.preventDefault) { e.preventDefault(); }
|
||||
this.options.shown = false;
|
||||
$body.removeClass('dialog-is-shown');
|
||||
return this.render();
|
||||
},
|
||||
hideAndRemove: function(e) {
|
||||
if(e && e.preventDefault) { e.preventDefault(); }
|
||||
return this.hide().remove();
|
||||
},
|
||||
upload: function(e) {
|
||||
if(e && e.preventDefault) { e.preventDefault(); }
|
||||
this.model.set('uploading', true);
|
||||
this.$("form").ajaxSubmit({
|
||||
success: _.bind(this.success, this),
|
||||
error: _.bind(this.error, this),
|
||||
uploadProgress: _.bind(this.progress, this),
|
||||
data: {
|
||||
// don't show the generic error notification; we're in a modal,
|
||||
// and we're better off modifying it instead.
|
||||
notifyOnError: false
|
||||
}
|
||||
});
|
||||
},
|
||||
progress: function(event, position, total, percentComplete) {
|
||||
this.model.set({
|
||||
"uploadedBytes": position,
|
||||
"totalBytes": total
|
||||
});
|
||||
},
|
||||
success: function(response, statusText, xhr, form) {
|
||||
this.model.set({
|
||||
uploading: false,
|
||||
finished: true
|
||||
});
|
||||
if(this.options.onSuccess) {
|
||||
this.options.onSuccess(response, statusText, xhr, form);
|
||||
}
|
||||
var that = this;
|
||||
this.removalTimeout = setTimeout(function() {
|
||||
that.hide().remove();
|
||||
}, this.options.successMessageTimeout);
|
||||
},
|
||||
error: function() {
|
||||
this.model.set({
|
||||
"uploading": false,
|
||||
"uploadedBytes": 0,
|
||||
"title": gettext("We're sorry, there was an error")
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -59,6 +59,7 @@
|
||||
@import 'views/users';
|
||||
@import 'views/checklists';
|
||||
@import 'views/textbooks';
|
||||
@import 'views/uploads';
|
||||
|
||||
// temp - inherited
|
||||
@import 'assets/content-types';
|
||||
|
||||
@@ -370,213 +370,4 @@ body.course.textbooks {
|
||||
.content-supplementary {
|
||||
width: flex-grid(3, 12);
|
||||
}
|
||||
|
||||
// dialog
|
||||
.wrapper-dialog {
|
||||
@extend .ui-depth5;
|
||||
@include transition(all 0.05s ease-in-out);
|
||||
position: fixed;
|
||||
top: 0;
|
||||
background: $black-t2;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
text-align: center;
|
||||
|
||||
&:before {
|
||||
content: '';
|
||||
display: inline-block;
|
||||
height: 100%;
|
||||
vertical-align: middle;
|
||||
margin-right: -0.25em; /* Adjusts for spacing */
|
||||
}
|
||||
|
||||
.dialog {
|
||||
@include box-sizing(border-box);
|
||||
box-shadow: 0px 0px 7px $shadow-d1;
|
||||
border-radius: ($baseline/5);
|
||||
background-color: $gray-l4;
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
width: $baseline*23;
|
||||
padding: 7px;
|
||||
text-align: left;
|
||||
|
||||
.title {
|
||||
@extend .t-title5;
|
||||
margin-bottom: ($baseline/2);
|
||||
font-weight: 600;
|
||||
color: $black;
|
||||
}
|
||||
|
||||
.message {
|
||||
@extend .t-copy-sub2;
|
||||
color: $gray;
|
||||
}
|
||||
|
||||
.error {
|
||||
color: $white;
|
||||
}
|
||||
|
||||
form {
|
||||
padding: 0;
|
||||
|
||||
.form-content {
|
||||
box-shadow: 0 0 3px $shadow-d1;
|
||||
padding: ($baseline*1.5);
|
||||
background-color: $white;
|
||||
}
|
||||
|
||||
input[type="file"] {
|
||||
@extend .t-copy-sub2;
|
||||
}
|
||||
|
||||
.status-upload {
|
||||
height: 30px;
|
||||
margin-top: $baseline;
|
||||
|
||||
.wrapper-progress {
|
||||
box-shadow: inset 0 0 3px $shadow-d1;
|
||||
display: block;
|
||||
border-radius: ($baseline*0.75);
|
||||
background-color: $gray-l5;
|
||||
padding: 1px 8px 2px 8px;
|
||||
height: 25px;
|
||||
|
||||
progress {
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
width: 100%;
|
||||
border: none;
|
||||
border-radius: ($baseline*0.75);
|
||||
background-color: $gray-l5;
|
||||
|
||||
&::-webkit-progress-bar {
|
||||
background-color: transparent;
|
||||
border-radius: ($baseline*0.75);
|
||||
}
|
||||
|
||||
&::-webkit-progress-value {
|
||||
background-color: $pink;
|
||||
border-radius: ($baseline*0.75);
|
||||
}
|
||||
|
||||
&::-moz-progress-bar {
|
||||
background-color: $pink;
|
||||
border-radius: ($baseline*0.75);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.message-status {
|
||||
@include border-top-radius(2px);
|
||||
@include box-sizing(border-box);
|
||||
@include font-size(14);
|
||||
display: none;
|
||||
border-bottom: 2px solid $yellow;
|
||||
margin: 0 0 20px 0;
|
||||
padding: 10px 20px;
|
||||
font-weight: 500;
|
||||
background: $paleYellow;
|
||||
|
||||
.text {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
&.error {
|
||||
border-color: $red-d2;
|
||||
background: $red-l1;
|
||||
color: $white;
|
||||
}
|
||||
|
||||
&.confirm {
|
||||
border-color: $green-d2;
|
||||
background: $green-l1;
|
||||
color: $white;
|
||||
}
|
||||
|
||||
&.is-shown {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.actions {
|
||||
padding: ($baseline*0.75) $baseline ($baseline/2) $baseline;
|
||||
|
||||
|
||||
|
||||
.action-item {
|
||||
@extend .t-action4;
|
||||
display: inline-block;
|
||||
margin-right: ($baseline*0.75);
|
||||
|
||||
&:last-child {
|
||||
margin-right: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.action-primary {
|
||||
@include blue-button();
|
||||
@include font-size(12); // needed due to bad button mixins for now
|
||||
border-color: $blue-d1;
|
||||
color: $white;
|
||||
}
|
||||
|
||||
a {
|
||||
color: $blue;
|
||||
|
||||
&:hover {
|
||||
color: $blue-s2;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// ====================
|
||||
|
||||
// js enabled
|
||||
.js {
|
||||
|
||||
// dialog set-up
|
||||
.wrapper-dialog {
|
||||
visibility: hidden;
|
||||
pointer-events: none;
|
||||
|
||||
.dialog {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
// dialog showing/hiding
|
||||
&.dialog-is-shown {
|
||||
|
||||
.wrapper-dialog {
|
||||
-webkit-filter: blur(2px) grayscale(25%);
|
||||
filter: blur(2px) grayscale(25%);
|
||||
}
|
||||
|
||||
.wrapper-dialog.is-shown {
|
||||
visibility: visible;
|
||||
pointer-events: auto;
|
||||
|
||||
.dialog {
|
||||
opacity: 1.0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
209
cms/static/sass/views/_uploads.scss
Normal file
209
cms/static/sass/views/_uploads.scss
Normal file
@@ -0,0 +1,209 @@
|
||||
// studio - views - uploads
|
||||
// ========================
|
||||
|
||||
body.course.file-upload-dialog {
|
||||
|
||||
// dialog
|
||||
.wrapper-dialog {
|
||||
@extend .ui-depth5;
|
||||
@include transition(all 0.05s ease-in-out);
|
||||
position: fixed;
|
||||
top: 0;
|
||||
background: $black-t2;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
text-align: center;
|
||||
|
||||
&:before {
|
||||
content: '';
|
||||
display: inline-block;
|
||||
height: 100%;
|
||||
vertical-align: middle;
|
||||
margin-right: -0.25em; /* Adjusts for spacing */
|
||||
}
|
||||
|
||||
.dialog {
|
||||
@include box-sizing(border-box);
|
||||
box-shadow: 0px 0px 7px $shadow-d1;
|
||||
border-radius: ($baseline/5);
|
||||
background-color: $gray-l4;
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
width: $baseline*23;
|
||||
padding: 7px;
|
||||
text-align: left;
|
||||
|
||||
.title {
|
||||
@extend .t-title5;
|
||||
margin-bottom: ($baseline/2);
|
||||
font-weight: 600;
|
||||
color: $black;
|
||||
}
|
||||
|
||||
.message {
|
||||
@extend .t-copy-sub2;
|
||||
color: $gray;
|
||||
}
|
||||
|
||||
.error {
|
||||
color: $white;
|
||||
}
|
||||
|
||||
form {
|
||||
padding: 0;
|
||||
|
||||
.form-content {
|
||||
box-shadow: 0 0 3px $shadow-d1;
|
||||
padding: ($baseline*1.5);
|
||||
background-color: $white;
|
||||
}
|
||||
|
||||
input[type="file"] {
|
||||
@extend .t-copy-sub2;
|
||||
}
|
||||
|
||||
.status-upload {
|
||||
height: 30px;
|
||||
margin-top: $baseline;
|
||||
|
||||
.wrapper-progress {
|
||||
box-shadow: inset 0 0 3px $shadow-d1;
|
||||
display: block;
|
||||
border-radius: ($baseline*0.75);
|
||||
background-color: $gray-l5;
|
||||
padding: 1px 8px 2px 8px;
|
||||
height: 25px;
|
||||
|
||||
progress {
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
width: 100%;
|
||||
border: none;
|
||||
border-radius: ($baseline*0.75);
|
||||
background-color: $gray-l5;
|
||||
|
||||
&::-webkit-progress-bar {
|
||||
background-color: transparent;
|
||||
border-radius: ($baseline*0.75);
|
||||
}
|
||||
|
||||
&::-webkit-progress-value {
|
||||
background-color: $pink;
|
||||
border-radius: ($baseline*0.75);
|
||||
}
|
||||
|
||||
&::-moz-progress-bar {
|
||||
background-color: $pink;
|
||||
border-radius: ($baseline*0.75);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.message-status {
|
||||
@include border-top-radius(2px);
|
||||
@include box-sizing(border-box);
|
||||
@include font-size(14);
|
||||
display: none;
|
||||
border-bottom: 2px solid $yellow;
|
||||
margin: 0 0 20px 0;
|
||||
padding: 10px 20px;
|
||||
font-weight: 500;
|
||||
background: $paleYellow;
|
||||
|
||||
.text {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
&.error {
|
||||
border-color: $red-d2;
|
||||
background: $red-l1;
|
||||
color: $white;
|
||||
}
|
||||
|
||||
&.confirm {
|
||||
border-color: $green-d2;
|
||||
background: $green-l1;
|
||||
color: $white;
|
||||
}
|
||||
|
||||
&.is-shown {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.actions {
|
||||
padding: ($baseline*0.75) $baseline ($baseline/2) $baseline;
|
||||
|
||||
|
||||
|
||||
.action-item {
|
||||
@extend .t-action4;
|
||||
display: inline-block;
|
||||
margin-right: ($baseline*0.75);
|
||||
|
||||
&:last-child {
|
||||
margin-right: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.action-primary {
|
||||
@include blue-button();
|
||||
@include font-size(12); // needed due to bad button mixins for now
|
||||
border-color: $blue-d1;
|
||||
color: $white;
|
||||
}
|
||||
|
||||
a {
|
||||
color: $blue;
|
||||
|
||||
&:hover {
|
||||
color: $blue-s2;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// ====================
|
||||
|
||||
// js enabled
|
||||
.js {
|
||||
|
||||
// dialog set-up
|
||||
.wrapper-dialog {
|
||||
visibility: hidden;
|
||||
pointer-events: none;
|
||||
|
||||
.dialog {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
// dialog showing/hiding
|
||||
&.dialog-is-shown {
|
||||
|
||||
.wrapper-dialog {
|
||||
-webkit-filter: blur(2px) grayscale(25%);
|
||||
filter: blur(2px) grayscale(25%);
|
||||
}
|
||||
|
||||
.wrapper-dialog.is-shown {
|
||||
visibility: visible;
|
||||
pointer-events: auto;
|
||||
|
||||
.dialog {
|
||||
opacity: 1.0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,7 @@
|
||||
<%! from django.utils.translation import ugettext as _ %>
|
||||
|
||||
<%block name="title">${_("Textbooks")}</%block>
|
||||
<%block name="bodyclass">is-signedin course textbooks</%block>
|
||||
<%block name="bodyclass">is-signedin course textbooks file-upload-dialog</%block>
|
||||
|
||||
<%block name="header_extras">
|
||||
% for template_name in ["edit-textbook", "show-textbook", "edit-chapter", "no-textbooks", "upload-dialog"]:
|
||||
|
||||
Reference in New Issue
Block a user