TNL-213: Let Students Add Personal Notes to Course Content.
Co-Authored-By: Jean-Michel Claus <jmc@edx.org> Co-Authored-By: Brian Talbot <btalbot@edx.org> Co-Authored-By: Tim Babych <tim@edx.org> Co-Authored-By: Oleg Marshev <oleg@edx.org> Co-Authored-By: Chris Rodriguez <crodriguez@edx.org>
This commit is contained in:
@@ -72,7 +72,7 @@ class @Calculator
|
||||
.attr
|
||||
'title': text
|
||||
'aria-expanded': isExpanded
|
||||
.text text
|
||||
.find('.utility-control-label').text text
|
||||
|
||||
$calc.toggleClass 'closed'
|
||||
|
||||
|
||||
@@ -2,7 +2,6 @@ class @Courseware
|
||||
@prefix: ''
|
||||
|
||||
constructor: ->
|
||||
Courseware.prefix = $("meta[name='path_prefix']").attr('content')
|
||||
new Navigation
|
||||
Logger.bind()
|
||||
@render()
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
AjaxPrefix.addAjaxPrefix(jQuery, -> Courseware.prefix)
|
||||
AjaxPrefix.addAjaxPrefix(jQuery, -> $("meta[name='path_prefix']").attr('content'))
|
||||
|
||||
$ ->
|
||||
$.ajaxSetup
|
||||
|
||||
46
lms/static/js/edxnotes/collections/notes.js
Normal file
46
lms/static/js/edxnotes/collections/notes.js
Normal file
@@ -0,0 +1,46 @@
|
||||
;(function (define, undefined) {
|
||||
'use strict';
|
||||
define([
|
||||
'backbone', 'js/edxnotes/models/note'
|
||||
], function (Backbone, NoteModel) {
|
||||
var NotesCollection = Backbone.Collection.extend({
|
||||
model: NoteModel,
|
||||
|
||||
/**
|
||||
* Returns course structure from the list of notes.
|
||||
* @return {Object}
|
||||
*/
|
||||
getCourseStructure: (function () {
|
||||
var courseStructure = null;
|
||||
return function () {
|
||||
var chapters = {},
|
||||
sections = {},
|
||||
units = {};
|
||||
|
||||
if (!courseStructure) {
|
||||
this.each(function (note) {
|
||||
var chapter = note.get('chapter'),
|
||||
section = note.get('section'),
|
||||
unit = note.get('unit');
|
||||
|
||||
chapters[chapter.location] = chapter;
|
||||
sections[section.location] = section;
|
||||
units[unit.location] = units[unit.location] || [];
|
||||
units[unit.location].push(note);
|
||||
});
|
||||
|
||||
courseStructure = {
|
||||
chapters: _.sortBy(_.toArray(chapters), function (c) {return c.index;}),
|
||||
sections: sections,
|
||||
units: units
|
||||
};
|
||||
}
|
||||
|
||||
return courseStructure;
|
||||
};
|
||||
}())
|
||||
});
|
||||
|
||||
return NotesCollection;
|
||||
});
|
||||
}).call(this, define || RequireJS.define);
|
||||
12
lms/static/js/edxnotes/collections/tabs.js
Normal file
12
lms/static/js/edxnotes/collections/tabs.js
Normal file
@@ -0,0 +1,12 @@
|
||||
;(function (define, undefined) {
|
||||
'use strict';
|
||||
define([
|
||||
'backbone', 'js/edxnotes/models/tab'
|
||||
], function (Backbone, TabModel) {
|
||||
var TabsCollection = Backbone.Collection.extend({
|
||||
model: TabModel
|
||||
});
|
||||
|
||||
return TabsCollection;
|
||||
});
|
||||
}).call(this, define || RequireJS.define);
|
||||
59
lms/static/js/edxnotes/models/note.js
Normal file
59
lms/static/js/edxnotes/models/note.js
Normal file
@@ -0,0 +1,59 @@
|
||||
;(function (define) {
|
||||
'use strict';
|
||||
define(['backbone', 'underscore.string'], function (Backbone) {
|
||||
var NoteModel = Backbone.Model.extend({
|
||||
defaults: {
|
||||
'id': null,
|
||||
'created': '',
|
||||
'updated': '',
|
||||
'user': '',
|
||||
'usage_id': '',
|
||||
'course_id': '',
|
||||
'text': '',
|
||||
'quote': '',
|
||||
'ranges': [],
|
||||
'unit': {
|
||||
'display_name': '',
|
||||
'url': '',
|
||||
'location': ''
|
||||
},
|
||||
'section': {
|
||||
'display_name': '',
|
||||
'location': '',
|
||||
'children': []
|
||||
},
|
||||
'chapter': {
|
||||
'display_name': '',
|
||||
'location': '',
|
||||
'index': 0,
|
||||
'children': []
|
||||
},
|
||||
// Flag indicating current state of the note: expanded or collapsed.
|
||||
'is_expanded': false,
|
||||
// Flag indicating whether `More` link should be shown.
|
||||
'show_link': false
|
||||
},
|
||||
|
||||
textSize: 300,
|
||||
|
||||
initialize: function () {
|
||||
if (this.get('quote').length > this.textSize) {
|
||||
this.set('show_link', true);
|
||||
}
|
||||
},
|
||||
|
||||
getNoteText: function () {
|
||||
var message = this.get('quote');
|
||||
|
||||
if (!this.get('is_expanded') && this.get('show_link')) {
|
||||
message = _.str.prune(message, this.textSize);
|
||||
}
|
||||
|
||||
return message;
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
return NoteModel;
|
||||
});
|
||||
}).call(this, define || RequireJS.define);
|
||||
34
lms/static/js/edxnotes/models/tab.js
Normal file
34
lms/static/js/edxnotes/models/tab.js
Normal file
@@ -0,0 +1,34 @@
|
||||
;(function (define, undefined) {
|
||||
'use strict';
|
||||
define(['backbone'], function (Backbone) {
|
||||
var TabModel = Backbone.Model.extend({
|
||||
defaults: {
|
||||
'identifier': '',
|
||||
'name': '',
|
||||
'icon': '',
|
||||
'is_active': false,
|
||||
'is_closable': false
|
||||
},
|
||||
|
||||
activate: function () {
|
||||
this.collection.each(_.bind(function(model) {
|
||||
// Inactivate all other models.
|
||||
if (model !== this) {
|
||||
model.inactivate();
|
||||
}
|
||||
}, this));
|
||||
this.set('is_active', true);
|
||||
},
|
||||
|
||||
inactivate: function () {
|
||||
this.set('is_active', false);
|
||||
},
|
||||
|
||||
isActive: function () {
|
||||
return this.get('is_active');
|
||||
}
|
||||
});
|
||||
|
||||
return TabModel;
|
||||
});
|
||||
}).call(this, define || RequireJS.define);
|
||||
155
lms/static/js/edxnotes/plugins/events.js
Normal file
155
lms/static/js/edxnotes/plugins/events.js
Normal file
@@ -0,0 +1,155 @@
|
||||
;(function (define, undefined) {
|
||||
'use strict';
|
||||
define([
|
||||
'underscore', 'annotator', 'underscore.string'
|
||||
], function (_, Annotator) {
|
||||
/**
|
||||
* Modifies Annotator.Plugin.Store.annotationCreated to make it trigger a new
|
||||
* event `annotationFullyCreated` when annotation is fully created and has
|
||||
* an id.
|
||||
*/
|
||||
Annotator.Plugin.Store.prototype.annotationCreated = _.compose(
|
||||
function (jqXhr) {
|
||||
return jqXhr.done(_.bind(function (annotation) {
|
||||
if (annotation && annotation.id){
|
||||
this.publish('annotationFullyCreated', annotation);
|
||||
}
|
||||
}, this));
|
||||
},
|
||||
Annotator.Plugin.Store.prototype.annotationCreated
|
||||
);
|
||||
|
||||
/**
|
||||
* Adds the Events Plugin which emits events to capture user intent.
|
||||
* Emits the following events:
|
||||
* - 'edx.course.student_notes.viewed'
|
||||
* [(user, note ID, datetime), (user, note ID, datetime)] - a list of notes.
|
||||
* - 'edx.course.student_notes.added'
|
||||
* (user, note ID, note text, highlighted content, ID of the component annotated, datetime)
|
||||
* - 'edx.course.student_notes.edited'
|
||||
* (user, note ID, old note text, new note text, highlighted content, ID of the component annotated, datetime)
|
||||
* - 'edx.course.student_notes.deleted'
|
||||
* (user, note ID, note text, highlighted content, ID of the component annotated, datetime)
|
||||
**/
|
||||
Annotator.Plugin.Events = function () {
|
||||
// Call the Annotator.Plugin constructor this sets up the element and
|
||||
// options properties.
|
||||
Annotator.Plugin.apply(this, arguments);
|
||||
};
|
||||
|
||||
_.extend(Annotator.Plugin.Events.prototype, new Annotator.Plugin(), {
|
||||
pluginInit: function () {
|
||||
_.bindAll(this,
|
||||
'annotationViewerShown', 'annotationFullyCreated', 'annotationEditorShown',
|
||||
'annotationEditorHidden', 'annotationUpdated', 'annotationDeleted'
|
||||
);
|
||||
|
||||
this.annotator
|
||||
.subscribe('annotationViewerShown', this.annotationViewerShown)
|
||||
.subscribe('annotationFullyCreated', this.annotationFullyCreated)
|
||||
.subscribe('annotationEditorShown', this.annotationEditorShown)
|
||||
.subscribe('annotationEditorHidden', this.annotationEditorHidden)
|
||||
.subscribe('annotationUpdated', this.annotationUpdated)
|
||||
.subscribe('annotationDeleted', this.annotationDeleted);
|
||||
},
|
||||
|
||||
destroy: function () {
|
||||
this.annotator
|
||||
.unsubscribe('annotationViewerShown', this.annotationViewerShown)
|
||||
.unsubscribe('annotationFullyCreated', this.annotationFullyCreated)
|
||||
.unsubscribe('annotationEditorShown', this.annotationEditorShown)
|
||||
.unsubscribe('annotationEditorHidden', this.annotationEditorHidden)
|
||||
.unsubscribe('annotationUpdated', this.annotationUpdated)
|
||||
.unsubscribe('annotationDeleted', this.annotationDeleted);
|
||||
},
|
||||
|
||||
annotationViewerShown: function (viewer, annotations) {
|
||||
// Emits an event only when the annotation already exists on the
|
||||
// server. Otherwise, `annotation.id` is `undefined`.
|
||||
var data;
|
||||
annotations = _.reject(annotations, this.isNew);
|
||||
data = {
|
||||
'notes': _.map(annotations, function (annotation) {
|
||||
return {'note_id': annotation.id};
|
||||
})
|
||||
};
|
||||
if (data.notes.length) {
|
||||
this.log('edx.course.student_notes.viewed', data);
|
||||
}
|
||||
},
|
||||
|
||||
annotationFullyCreated: function (annotation) {
|
||||
var data = this.getDefaultData(annotation);
|
||||
this.log('edx.course.student_notes.added', data);
|
||||
},
|
||||
|
||||
annotationEditorShown: function (editor, annotation) {
|
||||
this.oldNoteText = annotation.text || '';
|
||||
},
|
||||
|
||||
annotationEditorHidden: function () {
|
||||
this.oldNoteText = null;
|
||||
},
|
||||
|
||||
annotationUpdated: function (annotation) {
|
||||
var data;
|
||||
if (!this.isNew(annotation)) {
|
||||
data = _.extend(
|
||||
this.getDefaultData(annotation),
|
||||
this.getText('old_note_text', this.oldNoteText)
|
||||
);
|
||||
this.log('edx.course.student_notes.edited', data);
|
||||
}
|
||||
},
|
||||
|
||||
annotationDeleted: function (annotation) {
|
||||
var data;
|
||||
// Emits an event only when the annotation already exists on the
|
||||
// server.
|
||||
if (!this.isNew(annotation)) {
|
||||
data = this.getDefaultData(annotation);
|
||||
this.log('edx.course.student_notes.deleted', data);
|
||||
}
|
||||
},
|
||||
|
||||
getDefaultData: function (annotation) {
|
||||
return _.extend(
|
||||
{
|
||||
'note_id': annotation.id,
|
||||
'component_usage_id': annotation.usage_id
|
||||
},
|
||||
this.getText('note_text', annotation.text),
|
||||
this.getText('highlighted_content', annotation.quote)
|
||||
);
|
||||
},
|
||||
|
||||
getText: function (fieldName, text) {
|
||||
var info = {},
|
||||
truncated = false,
|
||||
limit = this.options.stringLimit;
|
||||
|
||||
if (_.isNumber(limit) && _.isString(text) && text.length > limit) {
|
||||
text = String(text).slice(0, limit);
|
||||
truncated = true;
|
||||
}
|
||||
|
||||
info[fieldName] = text;
|
||||
info[fieldName + '_truncated'] = truncated;
|
||||
|
||||
return info;
|
||||
},
|
||||
|
||||
/**
|
||||
* If the model does not yet have an id, it is considered to be new.
|
||||
* @return {Boolean}
|
||||
*/
|
||||
isNew: function (annotation) {
|
||||
return !_.has(annotation, 'id');
|
||||
},
|
||||
|
||||
log: function (eventName, data) {
|
||||
this.annotator.logger.emit(eventName, data);
|
||||
}
|
||||
});
|
||||
});
|
||||
}).call(this, define || RequireJS.define);
|
||||
65
lms/static/js/edxnotes/plugins/scroller.js
Normal file
65
lms/static/js/edxnotes/plugins/scroller.js
Normal file
@@ -0,0 +1,65 @@
|
||||
;(function (define, undefined) {
|
||||
'use strict';
|
||||
define(['jquery', 'underscore', 'annotator'], function ($, _, Annotator) {
|
||||
/**
|
||||
* Adds the Scroller Plugin which scrolls to a note with a certain id and
|
||||
* opens it.
|
||||
**/
|
||||
Annotator.Plugin.Scroller = function () {
|
||||
// Call the Annotator.Plugin constructor this sets up the element and
|
||||
// options properties.
|
||||
Annotator.Plugin.apply(this, arguments);
|
||||
};
|
||||
|
||||
$.extend(Annotator.Plugin.Scroller.prototype, new Annotator.Plugin(), {
|
||||
getIdFromLocationHash: function() {
|
||||
return window.location.hash.substr(1);
|
||||
},
|
||||
|
||||
pluginInit: function () {
|
||||
_.bindAll(this, 'onNotesLoaded');
|
||||
// If the page URL contains a hash, we could be coming from a click
|
||||
// on an anchor in the notes page. In that case, the hash is the id
|
||||
// of the note that has to be scrolled to and opened.
|
||||
if (this.getIdFromLocationHash()) {
|
||||
this.annotator.subscribe('annotationsLoaded', this.onNotesLoaded);
|
||||
}
|
||||
},
|
||||
|
||||
destroy: function () {
|
||||
this.annotator.unsubscribe('annotationsLoaded', this.onNotesLoaded);
|
||||
},
|
||||
|
||||
onNotesLoaded: function (notes) {
|
||||
var hash = this.getIdFromLocationHash();
|
||||
this.annotator.logger.log('Scroller', {
|
||||
'notes:': notes,
|
||||
'hash': hash
|
||||
});
|
||||
_.each(notes, function (note) {
|
||||
var highlight, offset;
|
||||
if (note.id === hash && note.highlights.length) {
|
||||
// Clear the page URL hash, it won't be needed once we've
|
||||
// scrolled and opened the relevant note. And it would
|
||||
// unnecessarily repeat the steps below if we come from
|
||||
// another sequential.
|
||||
window.location.hash = '';
|
||||
highlight = $(note.highlights[0]);
|
||||
offset = highlight.position();
|
||||
// Open the note
|
||||
this.annotator.showFrozenViewer([note], {
|
||||
top: offset.top + 0.5 * highlight.height(),
|
||||
left: offset.left + 0.5 * highlight.width()
|
||||
});
|
||||
// Scroll to highlight
|
||||
this.scrollIntoView(highlight);
|
||||
}
|
||||
}, this);
|
||||
},
|
||||
|
||||
scrollIntoView: function (highlight) {
|
||||
highlight.focus();
|
||||
}
|
||||
});
|
||||
});
|
||||
}).call(this, define || RequireJS.define);
|
||||
150
lms/static/js/edxnotes/utils/logger.js
Normal file
150
lms/static/js/edxnotes/utils/logger.js
Normal file
@@ -0,0 +1,150 @@
|
||||
;(function (define) {
|
||||
'use strict';
|
||||
define(['underscore', 'logger'], function (_, Logger) {
|
||||
var loggers = [],
|
||||
NotesLogger, now, destroyLogger;
|
||||
|
||||
now = function () {
|
||||
if (performance && performance.now) {
|
||||
return performance.now();
|
||||
} else if (Date.now) {
|
||||
return Date.now();
|
||||
} else {
|
||||
return (new Date()).getTime();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Removes a reference on the logger from `loggers`.
|
||||
* @param {Object} logger An instance of Logger.
|
||||
*/
|
||||
destroyLogger = function (logger) {
|
||||
var index = loggers.length,
|
||||
removedLogger;
|
||||
|
||||
while(index--) {
|
||||
if (loggers[index].id === logger.id) {
|
||||
removedLogger = loggers.splice(index, 1)[0];
|
||||
removedLogger.historyStorage = [];
|
||||
removedLogger.timeStorage = {};
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* NotesLogger constructor.
|
||||
* @constructor
|
||||
* @param {String} id Id of the logger.
|
||||
* @param {Boolean|Number} mode Outputs messages to the Web Console if true.
|
||||
*/
|
||||
NotesLogger = function (id, mode) {
|
||||
this.id = id;
|
||||
this.historyStorage = [];
|
||||
this.timeStorage = {};
|
||||
// 0 - silent;
|
||||
// 1 - show logs;
|
||||
this.logLevel = mode;
|
||||
};
|
||||
|
||||
/**
|
||||
* Outputs a message with appropriate type to the Web Console and
|
||||
* store it in the history.
|
||||
* @param {String} logType The type of the log message.
|
||||
* @param {Arguments} args Information that will be stored.
|
||||
*/
|
||||
NotesLogger.prototype._log = function (logType, args) {
|
||||
if (!this.logLevel) {
|
||||
return false;
|
||||
}
|
||||
this.updateHistory.apply(this, arguments);
|
||||
// Adds ID at the first place
|
||||
Array.prototype.unshift.call(args, this.id);
|
||||
if (console && console[logType]) {
|
||||
if (console[logType].apply){
|
||||
console[logType].apply(console, args);
|
||||
} else { // Do this for IE
|
||||
console[logType](args.join(' '));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Outputs a message to the Web Console and store it in the history.
|
||||
*/
|
||||
NotesLogger.prototype.log = function () {
|
||||
this._log('log', arguments);
|
||||
};
|
||||
|
||||
/**
|
||||
* Outputs an error message to the Web Console and store it in the history.
|
||||
*/
|
||||
NotesLogger.prototype.error = function () {
|
||||
this._log('error', arguments);
|
||||
};
|
||||
|
||||
/**
|
||||
* Adds information to the history.
|
||||
*/
|
||||
NotesLogger.prototype.updateHistory = function () {
|
||||
this.historyStorage.push(arguments);
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns the history for the logger.
|
||||
* @return {Array}
|
||||
*/
|
||||
NotesLogger.prototype.getHistory = function () {
|
||||
return this.historyStorage;
|
||||
};
|
||||
|
||||
/**
|
||||
* Starts a timer you can use to track how long an operation takes.
|
||||
* @param {String} label Timer name.
|
||||
*/
|
||||
NotesLogger.prototype.time = function (label) {
|
||||
this.timeStorage[label] = now();
|
||||
};
|
||||
|
||||
/**
|
||||
* Stops a timer that was previously started by calling NotesLogger.prototype.time().
|
||||
* @param {String} label Timer name.
|
||||
*/
|
||||
NotesLogger.prototype.timeEnd = function (label) {
|
||||
if (!this.timeStorage[label]) {
|
||||
return null;
|
||||
}
|
||||
|
||||
this._log('log', [label, now() - this.timeStorage[label], 'ms']);
|
||||
delete this.timeStorage[label];
|
||||
};
|
||||
|
||||
NotesLogger.prototype.destroy = function () {
|
||||
destroyLogger(this);
|
||||
};
|
||||
|
||||
/**
|
||||
* Emits the event.
|
||||
* @param {String} eventName The name of the event.
|
||||
* @param {*} data Information about the event.
|
||||
* @param {Number} timeout Optional timeout for the ajax request in ms.
|
||||
*/
|
||||
NotesLogger.prototype.emit = function (eventName, data, timeout) {
|
||||
var args = [eventName, data];
|
||||
this.log(eventName, data);
|
||||
if (timeout) {
|
||||
args.push(null, {'timeout': timeout});
|
||||
}
|
||||
return Logger.log.apply(Logger, args);
|
||||
};
|
||||
|
||||
return {
|
||||
getLogger: function (id, mode) {
|
||||
var logger = new NotesLogger(id, mode);
|
||||
loggers.push(logger);
|
||||
return logger;
|
||||
},
|
||||
destroyLogger: destroyLogger
|
||||
};
|
||||
});
|
||||
}).call(this, define || RequireJS.define);
|
||||
22
lms/static/js/edxnotes/utils/template.js
Normal file
22
lms/static/js/edxnotes/utils/template.js
Normal file
@@ -0,0 +1,22 @@
|
||||
;(function (define, undefined) {
|
||||
'use strict';
|
||||
define(['jquery', 'underscore'], function($, _) {
|
||||
/**
|
||||
* Loads the named template from the page, or logs an error if it fails.
|
||||
* @param name The name of the template.
|
||||
* @return The loaded template.
|
||||
*/
|
||||
var loadTemplate = function(name) {
|
||||
var templateSelector = '#' + name + '-tpl',
|
||||
templateText = $(templateSelector).text();
|
||||
if (!templateText) {
|
||||
console.error('Failed to load ' + name + ' template');
|
||||
}
|
||||
return _.template(templateText);
|
||||
};
|
||||
|
||||
return {
|
||||
loadTemplate: loadTemplate
|
||||
};
|
||||
});
|
||||
}).call(this, define || RequireJS.define);
|
||||
70
lms/static/js/edxnotes/views/note_group.js
Normal file
70
lms/static/js/edxnotes/views/note_group.js
Normal file
@@ -0,0 +1,70 @@
|
||||
;(function (define, undefined) {
|
||||
'use strict';
|
||||
define([
|
||||
'gettext', 'underscore', 'backbone'
|
||||
], function (gettext, _, Backbone) {
|
||||
var NoteSectionView, NoteGroupView;
|
||||
|
||||
NoteSectionView = Backbone.View.extend({
|
||||
tagName: 'section',
|
||||
className: 'note-section',
|
||||
id: function () {
|
||||
return 'note-section-' + _.uniqueId();
|
||||
},
|
||||
template: _.template('<h4 class="course-subtitle"><%- sectionName %></h4>'),
|
||||
|
||||
render: function () {
|
||||
this.$el.prepend(this.template({
|
||||
sectionName: this.options.section.display_name
|
||||
}));
|
||||
|
||||
return this;
|
||||
},
|
||||
|
||||
addChild: function (child) {
|
||||
this.$el.append(child);
|
||||
}
|
||||
});
|
||||
|
||||
NoteGroupView = Backbone.View.extend({
|
||||
tagName: 'section',
|
||||
className: 'note-group',
|
||||
id: function () {
|
||||
return 'note-group-' + _.uniqueId();
|
||||
},
|
||||
template: _.template('<h3 class="course-title"><%- chapterName %></h3>'),
|
||||
|
||||
initialize: function () {
|
||||
this.children = [];
|
||||
},
|
||||
|
||||
render: function () {
|
||||
var container = document.createDocumentFragment();
|
||||
this.$el.html(this.template({
|
||||
chapterName: this.options.chapter.display_name || ''
|
||||
}));
|
||||
_.each(this.children, function (section) {
|
||||
container.appendChild(section.render().el);
|
||||
});
|
||||
this.$el.append(container);
|
||||
|
||||
return this;
|
||||
},
|
||||
|
||||
addChild: function (sectionInfo) {
|
||||
var section = new NoteSectionView({section: sectionInfo});
|
||||
this.children.push(section);
|
||||
return section;
|
||||
},
|
||||
|
||||
remove: function () {
|
||||
_.invoke(this.children, 'remove');
|
||||
this.children = null;
|
||||
Backbone.View.prototype.remove.call(this);
|
||||
return this;
|
||||
}
|
||||
});
|
||||
|
||||
return NoteGroupView;
|
||||
});
|
||||
}).call(this, define || RequireJS.define);
|
||||
71
lms/static/js/edxnotes/views/note_item.js
Normal file
71
lms/static/js/edxnotes/views/note_item.js
Normal file
@@ -0,0 +1,71 @@
|
||||
;(function (define, undefined) {
|
||||
'use strict';
|
||||
define([
|
||||
'jquery', 'underscore','backbone', 'js/edxnotes/utils/template',
|
||||
'js/edxnotes/utils/logger'
|
||||
], function ($, _, Backbone, templateUtils, NotesLogger) {
|
||||
var NoteItemView = Backbone.View.extend({
|
||||
tagName: 'article',
|
||||
className: 'note',
|
||||
id: function () {
|
||||
return 'note-' + _.uniqueId();
|
||||
},
|
||||
events: {
|
||||
'click .note-excerpt-more-link': 'moreHandler',
|
||||
'click .reference-unit-link': 'unitLinkHandler',
|
||||
},
|
||||
|
||||
initialize: function (options) {
|
||||
this.template = templateUtils.loadTemplate('note-item');
|
||||
this.logger = NotesLogger.getLogger('note_item', options.debug);
|
||||
this.listenTo(this.model, 'change:is_expanded', this.render);
|
||||
},
|
||||
|
||||
render: function () {
|
||||
var context = this.getContext();
|
||||
this.$el.html(this.template(context));
|
||||
|
||||
return this;
|
||||
},
|
||||
|
||||
getContext: function () {
|
||||
return $.extend({
|
||||
message: this.model.getNoteText()
|
||||
}, this.model.toJSON());
|
||||
},
|
||||
|
||||
toggleNote: function () {
|
||||
var value = !this.model.get('is_expanded');
|
||||
this.model.set('is_expanded', value);
|
||||
},
|
||||
|
||||
moreHandler: function (event) {
|
||||
event.preventDefault();
|
||||
this.toggleNote();
|
||||
},
|
||||
|
||||
unitLinkHandler: function (event) {
|
||||
var REQUEST_TIMEOUT = 2000;
|
||||
event.preventDefault();
|
||||
this.logger.emit('edx.student_notes.used_unit_link', {
|
||||
'note_id': this.model.get('id'),
|
||||
'component_usage_id': this.model.get('usage_id')
|
||||
}, REQUEST_TIMEOUT).always(_.bind(function () {
|
||||
this.redirectTo(event.target.href);
|
||||
}, this));
|
||||
},
|
||||
|
||||
redirectTo: function (uri) {
|
||||
window.location = uri;
|
||||
},
|
||||
|
||||
remove: function () {
|
||||
this.logger.destroy();
|
||||
Backbone.View.prototype.remove.call(this);
|
||||
return this;
|
||||
}
|
||||
});
|
||||
|
||||
return NoteItemView;
|
||||
});
|
||||
}).call(this, define || RequireJS.define);
|
||||
94
lms/static/js/edxnotes/views/notes_factory.js
Normal file
94
lms/static/js/edxnotes/views/notes_factory.js
Normal file
@@ -0,0 +1,94 @@
|
||||
;(function (define, undefined) {
|
||||
'use strict';
|
||||
define([
|
||||
'jquery', 'underscore', 'annotator', 'js/edxnotes/utils/logger',
|
||||
'js/edxnotes/views/shim', 'js/edxnotes/plugins/scroller',
|
||||
'js/edxnotes/plugins/events'
|
||||
], function ($, _, Annotator, NotesLogger) {
|
||||
var plugins = ['Auth', 'Store', 'Scroller', 'Events'],
|
||||
getOptions, setupPlugins, updateHeaders, getAnnotator;
|
||||
|
||||
/**
|
||||
* Returns options for the annotator.
|
||||
* @param {jQuery Element} The container element.
|
||||
* @param {String} params.endpoint The endpoint of the store.
|
||||
* @param {String} params.user User id of annotation owner.
|
||||
* @param {String} params.usageId Usage Id of the component.
|
||||
* @param {String} params.courseId Course id.
|
||||
* @param {String} params.token An authentication token.
|
||||
* @param {String} params.tokenUrl The URL to request the token from.
|
||||
* @return {Object} Options.
|
||||
**/
|
||||
getOptions = function (element, params) {
|
||||
var defaultParams = {
|
||||
user: params.user,
|
||||
usage_id: params.usageId,
|
||||
course_id: params.courseId
|
||||
},
|
||||
prefix = params.endpoint.replace(/(.+)\/$/, '$1');
|
||||
|
||||
return {
|
||||
auth: {
|
||||
token: params.token,
|
||||
tokenUrl: params.tokenUrl
|
||||
},
|
||||
events: {
|
||||
stringLimit: 300
|
||||
},
|
||||
store: {
|
||||
prefix: prefix,
|
||||
annotationData: defaultParams,
|
||||
loadFromSearch: defaultParams,
|
||||
urls: {
|
||||
create: '/annotations/',
|
||||
read: '/annotations/:id/',
|
||||
update: '/annotations/:id/',
|
||||
destroy: '/annotations/:id/',
|
||||
search: '/search/'
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Setups plugins for the annotator.
|
||||
* @param {Object} annotator An instance of the annotator.
|
||||
* @param {Array} plugins A list of plugins for the annotator.
|
||||
* @param {Object} options An options for the annotator.
|
||||
**/
|
||||
setupPlugins = function (annotator, plugins, options) {
|
||||
_.each(plugins, function(plugin) {
|
||||
var settings = options[plugin.toLowerCase()];
|
||||
annotator.addPlugin(plugin, settings);
|
||||
}, this);
|
||||
};
|
||||
|
||||
/**
|
||||
* Factory method that returns Annotator.js instantiates.
|
||||
* @param {DOM Element} element The container element.
|
||||
* @param {String} params.endpoint The endpoint of the store.
|
||||
* @param {String} params.user User id of annotation owner.
|
||||
* @param {String} params.usageId Usage Id of the component.
|
||||
* @param {String} params.courseId Course id.
|
||||
* @param {String} params.token An authentication token.
|
||||
* @param {String} params.tokenUrl The URL to request the token from.
|
||||
* @return {Object} An instance of Annotator.js.
|
||||
**/
|
||||
getAnnotator = function (element, params) {
|
||||
var el = $(element),
|
||||
options = getOptions(el, params),
|
||||
logger = NotesLogger.getLogger(element.id, params.debug),
|
||||
annotator;
|
||||
|
||||
annotator = el.annotator(options).data('annotator');
|
||||
setupPlugins(annotator, plugins, options);
|
||||
annotator.logger = logger;
|
||||
logger.log({'element': element, 'options': options});
|
||||
return annotator;
|
||||
};
|
||||
|
||||
return {
|
||||
factory: getAnnotator
|
||||
};
|
||||
});
|
||||
}).call(this, define || RequireJS.define);
|
||||
44
lms/static/js/edxnotes/views/notes_page.js
Normal file
44
lms/static/js/edxnotes/views/notes_page.js
Normal file
@@ -0,0 +1,44 @@
|
||||
;(function (define, undefined) {
|
||||
'use strict';
|
||||
define([
|
||||
'backbone', 'js/edxnotes/collections/tabs', 'js/edxnotes/views/tabs_list',
|
||||
'js/edxnotes/views/tabs/recent_activity', 'js/edxnotes/views/tabs/course_structure',
|
||||
'js/edxnotes/views/tabs/search_results'
|
||||
], function (
|
||||
Backbone, TabsCollection, TabsListView, RecentActivityView, CourseStructureView,
|
||||
SearchResultsView
|
||||
) {
|
||||
var NotesPageView = Backbone.View.extend({
|
||||
initialize: function (options) {
|
||||
this.options = options;
|
||||
this.tabsCollection = new TabsCollection();
|
||||
|
||||
this.recentActivityView = new RecentActivityView({
|
||||
el: this.el,
|
||||
collection: this.collection,
|
||||
tabsCollection: this.tabsCollection
|
||||
});
|
||||
|
||||
this.courseStructureView = new CourseStructureView({
|
||||
el: this.el,
|
||||
collection: this.collection,
|
||||
tabsCollection: this.tabsCollection
|
||||
});
|
||||
|
||||
this.searchResultsView = new SearchResultsView({
|
||||
el: this.el,
|
||||
tabsCollection: this.tabsCollection,
|
||||
debug: this.options.debug,
|
||||
createTabOnInitialization: false
|
||||
});
|
||||
|
||||
this.tabsView = new TabsListView({collection: this.tabsCollection});
|
||||
this.$('.tab-list')
|
||||
.append(this.tabsView.render().$el)
|
||||
.removeClass('is-hidden');
|
||||
}
|
||||
});
|
||||
|
||||
return NotesPageView;
|
||||
});
|
||||
}).call(this, define || RequireJS.define);
|
||||
25
lms/static/js/edxnotes/views/page_factory.js
Normal file
25
lms/static/js/edxnotes/views/page_factory.js
Normal file
@@ -0,0 +1,25 @@
|
||||
;(function (define, undefined) {
|
||||
'use strict';
|
||||
define([
|
||||
'jquery', 'js/edxnotes/collections/notes', 'js/edxnotes/views/notes_page'
|
||||
], function ($, NotesCollection, NotesPageView) {
|
||||
/**
|
||||
* Factory method for the Notes page.
|
||||
* @param {Object} params Params for the Notes page.
|
||||
* @param {Array} params.notesList A list of note models.
|
||||
* @param {Boolean} params.debugMode Enable the flag to see debug information.
|
||||
* @param {String} params.endpoint The endpoint of the store.
|
||||
* @return {Object} An instance of NotesPageView.
|
||||
*/
|
||||
return function (params) {
|
||||
var collection = new NotesCollection(params.notesList);
|
||||
|
||||
return new NotesPageView({
|
||||
el: $('.wrapper-student-notes').get(0),
|
||||
collection: collection,
|
||||
debug: params.debugMode,
|
||||
endpoint: params.endpoint
|
||||
});
|
||||
};
|
||||
});
|
||||
}).call(this, define || RequireJS.define);
|
||||
161
lms/static/js/edxnotes/views/search_box.js
Normal file
161
lms/static/js/edxnotes/views/search_box.js
Normal file
@@ -0,0 +1,161 @@
|
||||
;(function (define, undefined) {
|
||||
'use strict';
|
||||
define([
|
||||
'jquery', 'underscore', 'backbone', 'gettext', 'js/edxnotes/utils/logger',
|
||||
'js/edxnotes/collections/notes'
|
||||
], function ($, _, Backbone, gettext, NotesLogger, NotesCollection) {
|
||||
var SearchBoxView = Backbone.View.extend({
|
||||
events: {
|
||||
'submit': 'submitHandler'
|
||||
},
|
||||
|
||||
errorMessage: gettext('An error has occurred. Make sure that you are connected to the Internet, and then try refreshing the page.'),
|
||||
emptyFieldMessage: (function () {
|
||||
var message = gettext('Please enter a term in the %(anchor_start)s search field%(anchor_end)s.');
|
||||
return interpolate(message, {
|
||||
'anchor_start': '<a href="#search-notes-input">',
|
||||
'anchor_end': '</a>'
|
||||
}, true);
|
||||
} ()),
|
||||
|
||||
initialize: function (options) {
|
||||
_.bindAll(this, 'onSuccess', 'onError', 'onComplete');
|
||||
this.options = _.defaults(options || {}, {
|
||||
beforeSearchStart: function () {},
|
||||
search: function () {},
|
||||
error: function () {},
|
||||
complete: function () {}
|
||||
});
|
||||
this.logger = NotesLogger.getLogger('search_box', this.options.debug);
|
||||
this.$el.removeClass('is-hidden');
|
||||
this.isDisabled = false;
|
||||
this.logger.log('initialized');
|
||||
},
|
||||
|
||||
submitHandler: function (event) {
|
||||
event.preventDefault();
|
||||
this.search();
|
||||
},
|
||||
|
||||
/**
|
||||
* Prepares server response to appropriate structure.
|
||||
* @param {Object} data The response form the server.
|
||||
* @return {Array}
|
||||
*/
|
||||
prepareData: function (data) {
|
||||
var collection;
|
||||
|
||||
if (!(data && _.has(data, 'total') && _.has(data, 'rows'))) {
|
||||
this.logger.log('Wrong data', data, this.searchQuery);
|
||||
return null;
|
||||
}
|
||||
|
||||
collection = new NotesCollection(data.rows);
|
||||
return [collection, data.total, this.searchQuery];
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns search text.
|
||||
* @return {String}
|
||||
*/
|
||||
getSearchQuery: function () {
|
||||
return this.$el.find('#search-notes-input').val();
|
||||
},
|
||||
|
||||
/**
|
||||
* Starts search if form is not disabled.
|
||||
* @return {Boolean} Indicates if search is started or not.
|
||||
*/
|
||||
search: function () {
|
||||
if (this.isDisabled) {
|
||||
return false;
|
||||
}
|
||||
|
||||
this.searchQuery = this.getSearchQuery();
|
||||
if (!this.validateField(this.searchQuery)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
this.options.beforeSearchStart(this.searchQuery);
|
||||
this.disableForm();
|
||||
this.sendRequest(this.searchQuery)
|
||||
.done(this.onSuccess)
|
||||
.fail(this.onError)
|
||||
.complete(this.onComplete);
|
||||
|
||||
return true;
|
||||
},
|
||||
|
||||
validateField: function (searchQuery) {
|
||||
if (!($.trim(searchQuery))) {
|
||||
this.options.error(this.emptyFieldMessage, searchQuery);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
|
||||
onSuccess: function (data) {
|
||||
var args = this.prepareData(data);
|
||||
if (args) {
|
||||
this.options.search.apply(this, args);
|
||||
this.logger.emit('edx.student_notes.searched', {
|
||||
'number_of_results': args[1],
|
||||
'search_string': args[2]
|
||||
});
|
||||
} else {
|
||||
this.options.error(this.errorMessage, this.searchQuery);
|
||||
}
|
||||
},
|
||||
|
||||
onError:function (jXHR) {
|
||||
var searchQuery = this.getSearchQuery(),
|
||||
message;
|
||||
|
||||
if (jXHR.responseText) {
|
||||
try {
|
||||
message = $.parseJSON(jXHR.responseText).error;
|
||||
} catch (error) { }
|
||||
}
|
||||
|
||||
this.options.error(message || this.errorMessage, searchQuery);
|
||||
this.logger.log('Response fails', jXHR.responseText);
|
||||
},
|
||||
|
||||
onComplete: function () {
|
||||
this.enableForm();
|
||||
this.options.complete(this.searchQuery);
|
||||
},
|
||||
|
||||
enableForm: function () {
|
||||
this.isDisabled = false;
|
||||
this.$el.removeClass('is-looking');
|
||||
this.$('button[type=submit]').removeClass('is-disabled');
|
||||
},
|
||||
|
||||
disableForm: function () {
|
||||
this.isDisabled = true;
|
||||
this.$el.addClass('is-looking');
|
||||
this.$('button[type=submit]').addClass('is-disabled');
|
||||
},
|
||||
|
||||
/**
|
||||
* Sends a request with appropriate configurations.
|
||||
* @param {String} text Search query.
|
||||
* @return {jQuery.Deferred}
|
||||
*/
|
||||
sendRequest: function (text) {
|
||||
var settings = {
|
||||
url: this.el.action,
|
||||
type: this.el.method,
|
||||
dataType: 'json',
|
||||
data: {text: text}
|
||||
};
|
||||
|
||||
this.logger.log(settings);
|
||||
return $.ajax(settings);
|
||||
}
|
||||
});
|
||||
|
||||
return SearchBoxView;
|
||||
});
|
||||
}).call(this, define || RequireJS.define);
|
||||
197
lms/static/js/edxnotes/views/shim.js
Normal file
197
lms/static/js/edxnotes/views/shim.js
Normal file
@@ -0,0 +1,197 @@
|
||||
;(function (define, undefined) {
|
||||
'use strict';
|
||||
define(['jquery', 'underscore', 'annotator'], function ($, _, Annotator) {
|
||||
var _t = Annotator._t;
|
||||
|
||||
/**
|
||||
* We currently run JQuery 1.7.2 in Jasmine tests and LMS.
|
||||
* AnnotatorJS 1.2.9. uses two calls to addBack (in the two functions
|
||||
* 'isAnnotator' and 'onHighlightMouseover') which was only defined in
|
||||
* JQuery 1.8.0. In LMS, it works without throwing an error because
|
||||
* JQuery.UI 1.10.0 adds support to jQuery<1.8 by augmenting '$.fn' with
|
||||
* that missing function. It is not the case for all Jasmine unit tests,
|
||||
* so we add it here if necessary.
|
||||
**/
|
||||
if (!$.fn.addBack) {
|
||||
$.fn.addBack = function (selector) {
|
||||
return this.add(
|
||||
selector === null ? this.prevObject : this.prevObject.filter(selector)
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* The original _setupDynamicStyle uses a very expensive call to
|
||||
* Util.maxZIndex(...) that sets the z-index of .annotator-adder,
|
||||
* .annotator-outer, .annotator-notice, .annotator-filter. We set these
|
||||
* values in annotator.min.css instead and do nothing here.
|
||||
*/
|
||||
Annotator.prototype._setupDynamicStyle = function() { };
|
||||
|
||||
Annotator.frozenSrc = null;
|
||||
|
||||
/**
|
||||
* Modifies Annotator.Plugin.Auth.haveValidToken to make it work with a new
|
||||
* token format.
|
||||
**/
|
||||
Annotator.Plugin.Auth.prototype.haveValidToken = function() {
|
||||
return (
|
||||
this._unsafeToken &&
|
||||
this._unsafeToken.sub &&
|
||||
this._unsafeToken.exp &&
|
||||
this._unsafeToken.iat &&
|
||||
this.timeToExpiry() > 0
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Modifies Annotator.Plugin.Auth.timeToExpiry to make it work with a new
|
||||
* token format.
|
||||
**/
|
||||
Annotator.Plugin.Auth.prototype.timeToExpiry = function() {
|
||||
var now = new Date().getTime() / 1000,
|
||||
expiry = this._unsafeToken.exp,
|
||||
timeToExpiry = expiry - now;
|
||||
|
||||
return (timeToExpiry > 0) ? timeToExpiry : 0;
|
||||
};
|
||||
|
||||
/**
|
||||
* Modifies Annotator.highlightRange to add a "tabindex=0" attribute
|
||||
* to the <span class="annotator-hl"> markup that encloses the note.
|
||||
* These are then focusable via the TAB key.
|
||||
**/
|
||||
Annotator.prototype.highlightRange = _.compose(
|
||||
function (results) {
|
||||
$('.annotator-hl', this.wrapper).attr('tabindex', 0);
|
||||
return results;
|
||||
},
|
||||
Annotator.prototype.highlightRange
|
||||
);
|
||||
|
||||
/**
|
||||
* Modifies Annotator.destroy to unbind click.edxnotes:freeze from the
|
||||
* document and reset isFrozen to default value, false.
|
||||
**/
|
||||
Annotator.prototype.destroy = _.compose(
|
||||
Annotator.prototype.destroy,
|
||||
function () {
|
||||
// We are destroying the instance that has the popup visible, revert to default,
|
||||
// unfreeze all instances and set their isFrozen to false
|
||||
if (this === Annotator.frozenSrc) {
|
||||
this.unfreezeAll();
|
||||
} else {
|
||||
// Unfreeze only this instance and unbound associated 'click.edxnotes:freeze' handler
|
||||
$(document).off('click.edxnotes:freeze' + this.uid);
|
||||
this.isFrozen = false;
|
||||
}
|
||||
|
||||
if (this.logger && this.logger.destroy) {
|
||||
this.logger.destroy();
|
||||
}
|
||||
// Unbind onNoteClick from click
|
||||
this.viewer.element.off('click', this.onNoteClick);
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* Modifies Annotator.Viewer.html.item template to add an i18n for the
|
||||
* buttons.
|
||||
**/
|
||||
Annotator.Viewer.prototype.html.item = [
|
||||
'<li class="annotator-annotation annotator-item">',
|
||||
'<span class="annotator-controls">',
|
||||
'<a href="#" title="', _t('View as webpage'), '" class="annotator-link">',
|
||||
_t('View as webpage'),
|
||||
'</a>',
|
||||
'<button title="', _t('Edit'), '" class="annotator-edit">',
|
||||
_t('Edit'),
|
||||
'</button>',
|
||||
'<button title="', _t('Delete'), '" class="annotator-delete">',
|
||||
_t('Delete'),
|
||||
'</button>',
|
||||
'</span>',
|
||||
'</li>'
|
||||
].join('');
|
||||
|
||||
/**
|
||||
* Modifies Annotator._setupViewer to add a "click" event on viewer.
|
||||
**/
|
||||
Annotator.prototype._setupViewer = _.compose(
|
||||
function () {
|
||||
this.viewer.element.on('click', _.bind(this.onNoteClick, this));
|
||||
return this;
|
||||
},
|
||||
Annotator.prototype._setupViewer
|
||||
);
|
||||
|
||||
$.extend(true, Annotator.prototype, {
|
||||
events: {
|
||||
'.annotator-hl click': 'onHighlightClick',
|
||||
'.annotator-viewer click': 'onNoteClick'
|
||||
},
|
||||
|
||||
isFrozen: false,
|
||||
uid: _.uniqueId(),
|
||||
|
||||
onHighlightClick: function (event) {
|
||||
Annotator.Util.preventEventDefault(event);
|
||||
|
||||
if (!this.isFrozen) {
|
||||
event.stopPropagation();
|
||||
this.onHighlightMouseover.call(this, event);
|
||||
}
|
||||
Annotator.frozenSrc = this;
|
||||
this.freezeAll();
|
||||
},
|
||||
|
||||
onNoteClick: function (event) {
|
||||
event.stopPropagation();
|
||||
Annotator.Util.preventEventDefault(event);
|
||||
if (!$(event.target).is('.annotator-delete')) {
|
||||
Annotator.frozenSrc = this;
|
||||
this.freezeAll();
|
||||
}
|
||||
},
|
||||
|
||||
freeze: function () {
|
||||
if (!this.isFrozen) {
|
||||
// Remove default events
|
||||
this.removeEvents();
|
||||
this.viewer.element.unbind('mouseover mouseout');
|
||||
this.uid = _.uniqueId();
|
||||
$(document).on('click.edxnotes:freeze' + this.uid, _.bind(this.unfreeze, this));
|
||||
this.isFrozen = true;
|
||||
}
|
||||
},
|
||||
|
||||
unfreeze: function () {
|
||||
if (this.isFrozen) {
|
||||
// Add default events
|
||||
this.addEvents();
|
||||
this.viewer.element.bind({
|
||||
'mouseover': this.clearViewerHideTimer,
|
||||
'mouseout': this.startViewerHideTimer
|
||||
});
|
||||
this.viewer.hide();
|
||||
$(document).off('click.edxnotes:freeze'+this.uid);
|
||||
this.isFrozen = false;
|
||||
Annotator.frozenSrc = null;
|
||||
}
|
||||
},
|
||||
|
||||
freezeAll: function () {
|
||||
_.invoke(Annotator._instances, 'freeze');
|
||||
},
|
||||
|
||||
unfreezeAll: function () {
|
||||
_.invoke(Annotator._instances, 'unfreeze');
|
||||
},
|
||||
|
||||
showFrozenViewer: function (annotations, location) {
|
||||
this.showViewer(annotations, location);
|
||||
this.freezeAll();
|
||||
}
|
||||
});
|
||||
});
|
||||
}).call(this, define || RequireJS.define);
|
||||
57
lms/static/js/edxnotes/views/tab_item.js
Normal file
57
lms/static/js/edxnotes/views/tab_item.js
Normal file
@@ -0,0 +1,57 @@
|
||||
;(function (define, undefined) {
|
||||
'use strict';
|
||||
define(['gettext', 'underscore', 'backbone', 'js/edxnotes/utils/template'],
|
||||
function (gettext, _, Backbone, templateUtils) {
|
||||
var TabItemView = Backbone.View.extend({
|
||||
tagName: 'li',
|
||||
className: 'tab',
|
||||
activeClassName: 'is-active',
|
||||
|
||||
events: {
|
||||
'click': 'selectHandler',
|
||||
'click a': function (event) { event.preventDefault(); },
|
||||
'click .action-close': 'closeHandler'
|
||||
},
|
||||
|
||||
initialize: function (options) {
|
||||
this.template = templateUtils.loadTemplate('tab-item');
|
||||
this.$el.attr('id', this.model.get('identifier'));
|
||||
this.listenTo(this.model, {
|
||||
'change:is_active': function (model, value) {
|
||||
this.$el.toggleClass(this.activeClassName, value);
|
||||
if (value) {
|
||||
this.$('.tab-label').prepend($('<span />', {
|
||||
'class': 'tab-aria-label sr',
|
||||
'text': gettext('Current tab')
|
||||
}));
|
||||
} else {
|
||||
this.$('.tab-aria-label').remove();
|
||||
}
|
||||
},
|
||||
'destroy': this.remove
|
||||
});
|
||||
},
|
||||
|
||||
render: function () {
|
||||
var html = this.template(this.model.toJSON());
|
||||
this.$el.html(html);
|
||||
return this;
|
||||
},
|
||||
|
||||
selectHandler: function (event) {
|
||||
event.preventDefault();
|
||||
if (!this.model.isActive()) {
|
||||
this.model.activate();
|
||||
}
|
||||
},
|
||||
|
||||
closeHandler: function (event) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
this.model.destroy();
|
||||
}
|
||||
});
|
||||
|
||||
return TabItemView;
|
||||
});
|
||||
}).call(this, define || RequireJS.define);
|
||||
54
lms/static/js/edxnotes/views/tab_panel.js
Normal file
54
lms/static/js/edxnotes/views/tab_panel.js
Normal file
@@ -0,0 +1,54 @@
|
||||
;(function (define, undefined) {
|
||||
'use strict';
|
||||
define(['gettext', 'underscore', 'backbone', 'js/edxnotes/views/note_item'],
|
||||
function (gettext, _, Backbone, NoteItemView) {
|
||||
var TabPanelView = Backbone.View.extend({
|
||||
tagName: 'section',
|
||||
className: 'tab-panel',
|
||||
title: '',
|
||||
titleTemplate: _.template('<h2 class="sr"><%- text %></h2>'),
|
||||
attributes: {
|
||||
'tabindex': -1
|
||||
},
|
||||
|
||||
initialize: function () {
|
||||
this.children = [];
|
||||
},
|
||||
|
||||
render: function () {
|
||||
this.$el.html(this.getTitle());
|
||||
this.renderContent();
|
||||
return this;
|
||||
},
|
||||
|
||||
renderContent: function () {
|
||||
return this;
|
||||
},
|
||||
|
||||
getNotes: function (collection) {
|
||||
var container = document.createDocumentFragment(),
|
||||
notes = _.map(collection, function (model) {
|
||||
var note = new NoteItemView({model: model});
|
||||
container.appendChild(note.render().el);
|
||||
return note;
|
||||
});
|
||||
|
||||
this.children = this.children.concat(notes);
|
||||
return container;
|
||||
},
|
||||
|
||||
getTitle: function () {
|
||||
return this.title ? this.titleTemplate({text: gettext(this.title)}) : '';
|
||||
},
|
||||
|
||||
remove: function () {
|
||||
_.invoke(this.children, 'remove');
|
||||
this.children = null;
|
||||
Backbone.View.prototype.remove.call(this);
|
||||
return this;
|
||||
}
|
||||
});
|
||||
|
||||
return TabPanelView;
|
||||
});
|
||||
}).call(this, define || RequireJS.define);
|
||||
138
lms/static/js/edxnotes/views/tab_view.js
Normal file
138
lms/static/js/edxnotes/views/tab_view.js
Normal file
@@ -0,0 +1,138 @@
|
||||
;(function (define, undefined) {
|
||||
'use strict';
|
||||
define([
|
||||
'underscore', 'backbone', 'js/edxnotes/models/tab'
|
||||
], function (_, Backbone, TabModel) {
|
||||
var TabView = Backbone.View.extend({
|
||||
PanelConstructor: null,
|
||||
|
||||
tabInfo: {
|
||||
name: '',
|
||||
class_name: ''
|
||||
},
|
||||
|
||||
initialize: function (options) {
|
||||
_.bindAll(this, 'showLoadingIndicator', 'hideLoadingIndicator');
|
||||
this.options = _.defaults(options || {}, {
|
||||
createTabOnInitialization: true
|
||||
});
|
||||
|
||||
if (this.options.createTabOnInitialization) {
|
||||
this.createTab();
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Creates a tab for the view.
|
||||
*/
|
||||
createTab: function () {
|
||||
this.tabModel = new TabModel(this.tabInfo);
|
||||
this.options.tabsCollection.add(this.tabModel);
|
||||
this.listenTo(this.tabModel, {
|
||||
'change:is_active': function (model, value) {
|
||||
if (value) {
|
||||
this.render();
|
||||
} else {
|
||||
this.destroySubView();
|
||||
}
|
||||
},
|
||||
'destroy': function () {
|
||||
this.destroySubView();
|
||||
this.tabModel = null;
|
||||
this.onClose();
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Renders content for the view.
|
||||
*/
|
||||
render: function () {
|
||||
this.hideErrorMessage().showLoadingIndicator();
|
||||
// If the view is already rendered, destroy it.
|
||||
this.destroySubView();
|
||||
this.renderContent().always(this.hideLoadingIndicator);
|
||||
return this;
|
||||
},
|
||||
|
||||
renderContent: function () {
|
||||
this.contentView = this.getSubView();
|
||||
this.$('.wrapper-tabs').append(this.contentView.render().$el);
|
||||
return $.Deferred().resolve().promise();
|
||||
},
|
||||
|
||||
getSubView: function () {
|
||||
var collection = this.getCollection();
|
||||
return new this.PanelConstructor({collection: collection});
|
||||
},
|
||||
|
||||
destroySubView: function () {
|
||||
if (this.contentView) {
|
||||
this.contentView.remove();
|
||||
this.contentView = null;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns collection for the view.
|
||||
* @return {Backbone.Collection}
|
||||
*/
|
||||
getCollection: function () {
|
||||
return this.collection;
|
||||
},
|
||||
|
||||
/**
|
||||
* Callback that is called on closing the tab.
|
||||
*/
|
||||
onClose: function () { },
|
||||
|
||||
/**
|
||||
* Returns the page's loading indicator.
|
||||
*/
|
||||
getLoadingIndicator: function() {
|
||||
return this.$('.ui-loading');
|
||||
},
|
||||
|
||||
/**
|
||||
* Shows the page's loading indicator.
|
||||
*/
|
||||
showLoadingIndicator: function() {
|
||||
this.getLoadingIndicator().removeClass('is-hidden');
|
||||
return this;
|
||||
},
|
||||
|
||||
/**
|
||||
* Hides the page's loading indicator.
|
||||
*/
|
||||
hideLoadingIndicator: function() {
|
||||
this.getLoadingIndicator().addClass('is-hidden');
|
||||
return this;
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Shows error message.
|
||||
*/
|
||||
showErrorMessage: function (message) {
|
||||
this.$('.wrapper-msg')
|
||||
.removeClass('is-hidden')
|
||||
.find('.msg-content .copy').html(message);
|
||||
|
||||
return this;
|
||||
},
|
||||
|
||||
/**
|
||||
* Hides error message.
|
||||
*/
|
||||
hideErrorMessage: function () {
|
||||
this.$('.wrapper-msg')
|
||||
.addClass('is-hidden')
|
||||
.find('.msg-content .copy').html('');
|
||||
|
||||
return this;
|
||||
}
|
||||
});
|
||||
|
||||
return TabView;
|
||||
});
|
||||
}).call(this, define || RequireJS.define);
|
||||
56
lms/static/js/edxnotes/views/tabs/course_structure.js
Normal file
56
lms/static/js/edxnotes/views/tabs/course_structure.js
Normal file
@@ -0,0 +1,56 @@
|
||||
;(function (define, undefined) {
|
||||
'use strict';
|
||||
define([
|
||||
'gettext', 'js/edxnotes/views/note_group', 'js/edxnotes/views/tab_panel',
|
||||
'js/edxnotes/views/tab_view'
|
||||
], function (gettext, NoteGroupView, TabPanelView, TabView) {
|
||||
var CourseStructureView = TabView.extend({
|
||||
PanelConstructor: TabPanelView.extend({
|
||||
id: 'structure-panel',
|
||||
title: 'Location in Course',
|
||||
|
||||
renderContent: function () {
|
||||
var courseStructure = this.collection.getCourseStructure(),
|
||||
container = document.createDocumentFragment();
|
||||
|
||||
_.each(courseStructure.chapters, function (chapterInfo) {
|
||||
var group = this.getGroup(chapterInfo);
|
||||
_.each(chapterInfo.children, function (location) {
|
||||
var sectionInfo = courseStructure.sections[location],
|
||||
section;
|
||||
if (sectionInfo) {
|
||||
section = group.addChild(sectionInfo);
|
||||
_.each(sectionInfo.children, function (location) {
|
||||
var notes = courseStructure.units[location];
|
||||
if (notes) {
|
||||
section.addChild(this.getNotes(notes))
|
||||
}
|
||||
}, this);
|
||||
}
|
||||
}, this);
|
||||
container.appendChild(group.render().el);
|
||||
}, this);
|
||||
this.$el.append(container);
|
||||
return this;
|
||||
},
|
||||
|
||||
getGroup: function (chapter, section) {
|
||||
var group = new NoteGroupView({
|
||||
chapter: chapter,
|
||||
section: section
|
||||
});
|
||||
this.children.push(group);
|
||||
return group;
|
||||
}
|
||||
}),
|
||||
|
||||
tabInfo: {
|
||||
name: gettext('Location in Course'),
|
||||
identifier: 'view-course-structure',
|
||||
icon: 'fa fa-list-ul'
|
||||
}
|
||||
});
|
||||
|
||||
return CourseStructureView;
|
||||
});
|
||||
}).call(this, define || RequireJS.define);
|
||||
31
lms/static/js/edxnotes/views/tabs/recent_activity.js
Normal file
31
lms/static/js/edxnotes/views/tabs/recent_activity.js
Normal file
@@ -0,0 +1,31 @@
|
||||
;(function (define, undefined) {
|
||||
'use strict';
|
||||
define([
|
||||
'gettext', 'js/edxnotes/views/tab_panel', 'js/edxnotes/views/tab_view'
|
||||
], function (gettext, TabPanelView, TabView) {
|
||||
var RecentActivityView = TabView.extend({
|
||||
PanelConstructor: TabPanelView.extend({
|
||||
id: 'recent-panel',
|
||||
title: 'Recent Activity',
|
||||
className: function () {
|
||||
return [
|
||||
TabPanelView.prototype.className,
|
||||
'note-group'
|
||||
].join(' ')
|
||||
},
|
||||
renderContent: function () {
|
||||
this.$el.append(this.getNotes(this.collection.toArray()));
|
||||
return this;
|
||||
}
|
||||
}),
|
||||
|
||||
tabInfo: {
|
||||
identifier: 'view-recent-activity',
|
||||
name: gettext('Recent Activity'),
|
||||
icon: 'fa fa-clock-o'
|
||||
}
|
||||
});
|
||||
|
||||
return RecentActivityView;
|
||||
});
|
||||
}).call(this, define || RequireJS.define);
|
||||
148
lms/static/js/edxnotes/views/tabs/search_results.js
Normal file
148
lms/static/js/edxnotes/views/tabs/search_results.js
Normal file
@@ -0,0 +1,148 @@
|
||||
;(function (define, undefined) {
|
||||
'use strict';
|
||||
define([
|
||||
'gettext', 'js/edxnotes/views/tab_panel', 'js/edxnotes/views/tab_view',
|
||||
'js/edxnotes/views/search_box'
|
||||
], function (gettext, TabPanelView, TabView, SearchBoxView) {
|
||||
var SearchResultsView = TabView.extend({
|
||||
PanelConstructor: TabPanelView.extend({
|
||||
id: 'search-results-panel',
|
||||
title: 'Search Results',
|
||||
className: function () {
|
||||
return [
|
||||
TabPanelView.prototype.className,
|
||||
'note-group'
|
||||
].join(' ');
|
||||
},
|
||||
renderContent: function () {
|
||||
this.$el.append(this.getNotes(this.collection.toArray()));
|
||||
return this;
|
||||
}
|
||||
}),
|
||||
|
||||
NoResultsViewConstructor: TabPanelView.extend({
|
||||
id: 'no-results-panel',
|
||||
title: 'No results found',
|
||||
className: function () {
|
||||
return [
|
||||
TabPanelView.prototype.className,
|
||||
'note-group'
|
||||
].join(' ');
|
||||
},
|
||||
renderContent: function () {
|
||||
var message = gettext('No results found for "%(query_string)s". Please try searching again.');
|
||||
|
||||
this.$el.append($('<p />', {
|
||||
text: interpolate(message, {
|
||||
query_string: this.options.searchQuery
|
||||
}, true)
|
||||
}));
|
||||
|
||||
return this;
|
||||
}
|
||||
}),
|
||||
|
||||
tabInfo: {
|
||||
identifier: 'view-search-results',
|
||||
name: gettext('Search Results'),
|
||||
icon: 'fa fa-search',
|
||||
is_closable: true
|
||||
},
|
||||
|
||||
initialize: function (options) {
|
||||
_.bindAll(this, 'onBeforeSearchStart', 'onSearch', 'onSearchError');
|
||||
TabView.prototype.initialize.call(this, options);
|
||||
this.searchResults = null;
|
||||
this.searchBox = new SearchBoxView({
|
||||
el: document.getElementById('search-notes-form'),
|
||||
debug: this.options.debug,
|
||||
beforeSearchStart: this.onBeforeSearchStart,
|
||||
search: this.onSearch,
|
||||
error: this.onSearchError
|
||||
});
|
||||
},
|
||||
|
||||
renderContent: function () {
|
||||
this.getLoadingIndicator().focus();
|
||||
return this.searchPromise.done(_.bind(function () {
|
||||
this.contentView = this.getSubView();
|
||||
if (this.contentView) {
|
||||
this.$('.wrapper-tabs').append(this.contentView.render().$el);
|
||||
}
|
||||
}, this));
|
||||
},
|
||||
|
||||
getSubView: function () {
|
||||
var collection = this.getCollection();
|
||||
if (collection) {
|
||||
if (collection.length) {
|
||||
return new this.PanelConstructor({
|
||||
collection: collection,
|
||||
searchQuery: this.searchResults.searchQuery
|
||||
});
|
||||
} else {
|
||||
return new this.NoResultsViewConstructor({
|
||||
searchQuery: this.searchResults.searchQuery
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
},
|
||||
|
||||
getCollection: function () {
|
||||
if (this.searchResults) {
|
||||
return this.searchResults.collection;
|
||||
}
|
||||
|
||||
return null;
|
||||
},
|
||||
|
||||
onClose: function () {
|
||||
this.searchResults = null;
|
||||
},
|
||||
|
||||
onBeforeSearchStart: function () {
|
||||
this.searchDeferred = $.Deferred();
|
||||
this.searchPromise = this.searchDeferred.promise();
|
||||
this.hideErrorMessage();
|
||||
this.searchResults = null;
|
||||
// If tab doesn't exist, creates it.
|
||||
if (!this.tabModel) {
|
||||
this.createTab();
|
||||
}
|
||||
// If tab is not already active, makes it active
|
||||
if (!this.tabModel.isActive()) {
|
||||
this.tabModel.activate();
|
||||
} else {
|
||||
this.render();
|
||||
}
|
||||
},
|
||||
|
||||
onSearch: function (collection, total, searchQuery) {
|
||||
this.searchResults = {
|
||||
collection: collection,
|
||||
total: total,
|
||||
searchQuery: searchQuery
|
||||
};
|
||||
|
||||
if (this.searchDeferred) {
|
||||
this.searchDeferred.resolve();
|
||||
}
|
||||
|
||||
if (this.contentView) {
|
||||
this.contentView.$el.focus();
|
||||
}
|
||||
},
|
||||
|
||||
onSearchError: function (errorMessage) {
|
||||
this.showErrorMessage(errorMessage);
|
||||
if (this.searchDeferred) {
|
||||
this.searchDeferred.reject();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return SearchResultsView;
|
||||
});
|
||||
}).call(this, define || RequireJS.define);
|
||||
41
lms/static/js/edxnotes/views/tabs_list.js
Normal file
41
lms/static/js/edxnotes/views/tabs_list.js
Normal file
@@ -0,0 +1,41 @@
|
||||
;(function (define, undefined) {
|
||||
'use strict';
|
||||
define([
|
||||
'underscore', 'backbone', 'js/edxnotes/views/tab_item'
|
||||
], function (_, Backbone, TabItemView) {
|
||||
var TabsListView = Backbone.View.extend({
|
||||
tagName: 'ul',
|
||||
className: 'tabs',
|
||||
|
||||
initialize: function (options) {
|
||||
this.options = options;
|
||||
this.listenTo(this.collection, {
|
||||
'add': this.createTab,
|
||||
'destroy': function (model, collection) {
|
||||
if (model.isActive() && collection.length) {
|
||||
collection.at(0).activate();
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
render: function () {
|
||||
this.collection.each(this.createTab, this);
|
||||
if (this.collection.length) {
|
||||
this.collection.at(0).activate();
|
||||
}
|
||||
return this;
|
||||
},
|
||||
|
||||
createTab: function (model) {
|
||||
var tab = new TabItemView({
|
||||
model: model
|
||||
});
|
||||
tab.render().$el.appendTo(this.$el);
|
||||
return tab;
|
||||
}
|
||||
});
|
||||
|
||||
return TabsListView;
|
||||
});
|
||||
}).call(this, define || RequireJS.define);
|
||||
98
lms/static/js/edxnotes/views/toggle_notes_factory.js
Normal file
98
lms/static/js/edxnotes/views/toggle_notes_factory.js
Normal file
@@ -0,0 +1,98 @@
|
||||
;(function (define, undefined) {
|
||||
'use strict';
|
||||
define([
|
||||
'jquery', 'underscore', 'backbone', 'gettext',
|
||||
'annotator', 'js/edxnotes/views/visibility_decorator'
|
||||
], function($, _, Backbone, gettext, Annotator, EdxnotesVisibilityDecorator) {
|
||||
var ToggleNotesView = Backbone.View.extend({
|
||||
events: {
|
||||
'click .action-toggle-notes': 'toggleHandler'
|
||||
},
|
||||
|
||||
errorMessage: gettext("An error has occurred. Make sure that you are connected to the Internet, and then try refreshing the page."),
|
||||
|
||||
initialize: function (options) {
|
||||
_.bindAll(this, 'onSuccess', 'onError');
|
||||
this.visibility = options.visibility;
|
||||
this.visibilityUrl = options.visibilityUrl;
|
||||
this.label = this.$('.utility-control-label');
|
||||
this.actionLink = this.$('.action-toggle-notes');
|
||||
this.actionLink.removeClass('is-disabled');
|
||||
this.actionToggleMessage = this.$('.action-toggle-message');
|
||||
this.notification = new Annotator.Notification();
|
||||
},
|
||||
|
||||
toggleHandler: function (event) {
|
||||
event.preventDefault();
|
||||
this.visibility = !this.visibility;
|
||||
this.showActionMessage();
|
||||
this.toggleNotes(this.visibility);
|
||||
},
|
||||
|
||||
toggleNotes: function (visibility) {
|
||||
if (visibility) {
|
||||
this.enableNotes();
|
||||
} else {
|
||||
this.disableNotes();
|
||||
}
|
||||
this.sendRequest();
|
||||
},
|
||||
|
||||
showActionMessage: function () {
|
||||
// The following lines are necessary to re-trigger the CSS animation on span.action-toggle-message
|
||||
this.actionToggleMessage.removeClass('is-fleeting');
|
||||
this.actionToggleMessage.offset().width = this.actionToggleMessage.offset().width;
|
||||
this.actionToggleMessage.addClass('is-fleeting');
|
||||
},
|
||||
|
||||
enableNotes: function () {
|
||||
_.each($('.edx-notes-wrapper'), EdxnotesVisibilityDecorator.enableNote);
|
||||
this.actionLink.addClass('is-active').attr('aria-pressed', true);
|
||||
this.label.text(gettext('Hide notes'));
|
||||
this.actionToggleMessage.text(gettext('Showing notes'));
|
||||
},
|
||||
|
||||
disableNotes: function () {
|
||||
EdxnotesVisibilityDecorator.disableNotes();
|
||||
this.actionLink.removeClass('is-active').attr('aria-pressed', false);
|
||||
this.label.text(gettext('Show notes'));
|
||||
this.actionToggleMessage.text(gettext('Hiding notes'));
|
||||
},
|
||||
|
||||
hideErrorMessage: function() {
|
||||
this.notification.hide();
|
||||
},
|
||||
|
||||
showErrorMessage: function(message) {
|
||||
this.notification.show(message, Annotator.Notification.ERROR);
|
||||
},
|
||||
|
||||
sendRequest: function () {
|
||||
return $.ajax({
|
||||
type: 'PUT',
|
||||
url: this.visibilityUrl,
|
||||
dataType: 'json',
|
||||
data: JSON.stringify({'visibility': this.visibility}),
|
||||
success: this.onSuccess,
|
||||
error: this.onError
|
||||
});
|
||||
},
|
||||
|
||||
onSuccess: function () {
|
||||
this.hideErrorMessage();
|
||||
},
|
||||
|
||||
onError: function () {
|
||||
this.showErrorMessage(this.errorMessage);
|
||||
}
|
||||
});
|
||||
|
||||
return function (visibility, visibilityUrl) {
|
||||
return new ToggleNotesView({
|
||||
el: $('.edx-notes-visibility').get(0),
|
||||
visibility: visibility,
|
||||
visibilityUrl: visibilityUrl
|
||||
});
|
||||
};
|
||||
});
|
||||
}).call(this, define || RequireJS.define);
|
||||
74
lms/static/js/edxnotes/views/visibility_decorator.js
Normal file
74
lms/static/js/edxnotes/views/visibility_decorator.js
Normal file
@@ -0,0 +1,74 @@
|
||||
;(function (define, undefined) {
|
||||
'use strict';
|
||||
define([
|
||||
'jquery', 'underscore', 'js/edxnotes/views/notes_factory'
|
||||
], function($, _, NotesFactory) {
|
||||
var parameters = {}, visibility = null,
|
||||
getIds, createNote, cleanup, factory;
|
||||
|
||||
getIds = function () {
|
||||
return _.map($('.edx-notes-wrapper'), function (element) {
|
||||
return element.id;
|
||||
});
|
||||
};
|
||||
|
||||
createNote = function (element, params) {
|
||||
if (params) {
|
||||
return NotesFactory.factory(element, params);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
cleanup = function (ids) {
|
||||
var list = _.clone(Annotator._instances);
|
||||
ids = ids || [];
|
||||
|
||||
_.each(list, function (instance) {
|
||||
var id = instance.element.attr('id');
|
||||
if (!_.contains(ids, id)) {
|
||||
instance.destroy();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
factory = function (element, params, isVisible) {
|
||||
// When switching sequentials, we need to keep track of the
|
||||
// parameters of each element and the visibility (that may have been
|
||||
// changed by the checkbox).
|
||||
parameters[element.id] = params;
|
||||
|
||||
if (_.isNull(visibility)) {
|
||||
visibility = isVisible;
|
||||
}
|
||||
|
||||
if (visibility) {
|
||||
// When switching sequentials, the global object Annotator still
|
||||
// keeps track of the previous instances that were created in an
|
||||
// array called 'Annotator._instances'. We have to destroy these
|
||||
// but keep those found on page being loaded (for the case when
|
||||
// there are more than one HTMLcomponent per vertical).
|
||||
cleanup(getIds());
|
||||
return createNote(element, params);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
return {
|
||||
factory: factory,
|
||||
|
||||
enableNote: function (element) {
|
||||
createNote(element, parameters[element.id]);
|
||||
visibility = true;
|
||||
},
|
||||
|
||||
disableNotes: function () {
|
||||
cleanup();
|
||||
visibility = false;
|
||||
},
|
||||
|
||||
_setVisibility: function (state) {
|
||||
visibility = state;
|
||||
},
|
||||
}
|
||||
});
|
||||
}).call(this, define || RequireJS.define);
|
||||
48
lms/static/js/fixtures/edxnotes/edxnotes.html
Normal file
48
lms/static/js/fixtures/edxnotes/edxnotes.html
Normal file
@@ -0,0 +1,48 @@
|
||||
<section class="container">
|
||||
<div class="wrapper-student-notes">
|
||||
<div class="student-notes">
|
||||
|
||||
<div class="title-search-container">
|
||||
<div class="wrapper-title">
|
||||
<h1 class="page-title">
|
||||
Notes
|
||||
<small class="page-subtitle">Highlights and notes you've made in course content</small>
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
<div class="wrapper-notes-search">
|
||||
<form role="search" action="/search_endpoint" method="GET" id="search-notes-form" class="is-hidden">
|
||||
<label for="search-notes-input" class="sr">Search notes for:</label>
|
||||
<input type="search" class="search-notes-input" id="search-notes-input" name="note" placeholder="Search notes for...">
|
||||
<button type="submit" class="search-notes-submit">
|
||||
<i class="icon fa fa-search"></i>
|
||||
<span class="sr">Search</span>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="wrapper-msg is-hidden error urgency-high inline-error">
|
||||
<div class="msg msg-error">
|
||||
<div class="msg-content">
|
||||
<p class="copy" aria-live="polite"></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<section class="wrapper-tabs">
|
||||
<div class="tab-list is-hidden">
|
||||
<h2 id="tab-view" class="tabs-label">View notes by</h2>
|
||||
</div>
|
||||
|
||||
<div class="ui-loading" tabindex="-1">
|
||||
<span class="spin">
|
||||
<i class="icon fa fa-refresh"></i>
|
||||
</span>
|
||||
<span class="copy">Loading</span>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
6
lms/static/js/fixtures/edxnotes/edxnotes_wrapper.html
Normal file
6
lms/static/js/fixtures/edxnotes/edxnotes_wrapper.html
Normal file
@@ -0,0 +1,6 @@
|
||||
<div id="edx-notes-wrapper-123" class="edx-notes-wrapper">
|
||||
<div class="edx-notes-wrapper-content">Annotate it!</div>
|
||||
</div>
|
||||
<div id="edx-notes-wrapper-456" class="edx-notes-wrapper">
|
||||
<div class="edx-notes-wrapper-content">Annotate it!</div>
|
||||
</div>
|
||||
7
lms/static/js/fixtures/edxnotes/toggle_notes.html
Normal file
7
lms/static/js/fixtures/edxnotes/toggle_notes.html
Normal file
@@ -0,0 +1,7 @@
|
||||
<div class="wrapper-utility edx-notes-visibility">
|
||||
<span class="action-toggle-message">Hiding notes</span>
|
||||
<button class="utility-control utility-control-button action-toggle-notes is-disabled is-active" aria-pressed="true">
|
||||
<i class="icon fa fa-pencil"></i>
|
||||
<span class="utility-control-label sr">Hide notes</span>
|
||||
</button>
|
||||
</div>
|
||||
34
lms/static/js/spec/edxnotes/collections/notes_spec.js
Normal file
34
lms/static/js/spec/edxnotes/collections/notes_spec.js
Normal file
@@ -0,0 +1,34 @@
|
||||
define([
|
||||
'js/spec/edxnotes/helpers', 'js/edxnotes/collections/notes'
|
||||
], function(Helpers, NotesCollection) {
|
||||
'use strict';
|
||||
describe('EdxNotes NotesCollection', function() {
|
||||
var notes = Helpers.getDefaultNotes();
|
||||
|
||||
beforeEach(function () {
|
||||
this.collection = new NotesCollection(notes);
|
||||
});
|
||||
|
||||
it('can return correct course structure', function () {
|
||||
var structure = this.collection.getCourseStructure();
|
||||
|
||||
expect(structure.chapters).toEqual([
|
||||
Helpers.getChapter('First Chapter', 1, 0, [2]),
|
||||
Helpers.getChapter('Second Chapter', 0, 1, [1, 'w_n', 0])
|
||||
]);
|
||||
|
||||
expect(structure.sections).toEqual({
|
||||
'i4x://section/0': Helpers.getSection('Third Section', 0, ['w_n', 1, 0]),
|
||||
'i4x://section/1': Helpers.getSection('Second Section', 1, [2]),
|
||||
'i4x://section/2': Helpers.getSection('First Section', 2, [3])
|
||||
});
|
||||
|
||||
expect(structure.units).toEqual({
|
||||
'i4x://unit/0': [this.collection.at(0), this.collection.at(1)],
|
||||
'i4x://unit/1': [this.collection.at(2)],
|
||||
'i4x://unit/2': [this.collection.at(3)],
|
||||
'i4x://unit/3': [this.collection.at(4)]
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
32
lms/static/js/spec/edxnotes/custom_matchers.js
Normal file
32
lms/static/js/spec/edxnotes/custom_matchers.js
Normal file
@@ -0,0 +1,32 @@
|
||||
define(['jquery'], function($) {
|
||||
'use strict';
|
||||
return function (that) {
|
||||
that.addMatchers({
|
||||
toContainText: function (text) {
|
||||
var trimmedText = $.trim($(this.actual).text());
|
||||
|
||||
if (text && $.isFunction(text.test)) {
|
||||
return text.test(trimmedText);
|
||||
} else {
|
||||
return trimmedText.indexOf(text) !== -1;
|
||||
}
|
||||
},
|
||||
|
||||
toHaveLength: function (number) {
|
||||
return $(this.actual).length === number;
|
||||
},
|
||||
|
||||
toHaveIndex: function (number) {
|
||||
return $(this.actual).index() === number;
|
||||
},
|
||||
|
||||
toBeInRange: function (min, max) {
|
||||
return min <= this.actual && this.actual <= max;
|
||||
},
|
||||
|
||||
toBeFocused: function () {
|
||||
return $(this.actual)[0] === $(this.actual)[0].ownerDocument.activeElement;
|
||||
}
|
||||
});
|
||||
};
|
||||
});
|
||||
169
lms/static/js/spec/edxnotes/helpers.js
Normal file
169
lms/static/js/spec/edxnotes/helpers.js
Normal file
@@ -0,0 +1,169 @@
|
||||
define(['underscore'], function(_) {
|
||||
'use strict';
|
||||
var B64 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=",
|
||||
LONG_TEXT, PRUNED_TEXT, TRUNCATED_TEXT, SHORT_TEXT,
|
||||
base64Encode, makeToken, getChapter, getSection, getUnit, getDefaultNotes;
|
||||
|
||||
LONG_TEXT = [
|
||||
'Adipisicing elit, sed do eiusmod tempor incididunt ',
|
||||
'ut labore et dolore magna aliqua. Ut enim ad minim ',
|
||||
'veniam, quis nostrud exercitation ullamco laboris ',
|
||||
'nisi ut aliquip ex ea commodo consequat. Duis aute ',
|
||||
'irure dolor in reprehenderit in voluptate velit esse ',
|
||||
'cillum dolore eu fugiat nulla pariatur. Excepteur ',
|
||||
'sint occaecat cupidatat non proident, sunt in culpa ',
|
||||
'qui officia deserunt mollit anim id est laborum.'
|
||||
].join('');
|
||||
PRUNED_TEXT = [
|
||||
'Adipisicing elit, sed do eiusmod tempor incididunt ',
|
||||
'ut labore et dolore magna aliqua. Ut enim ad minim ',
|
||||
'veniam, quis nostrud exercitation ullamco laboris ',
|
||||
'nisi ut aliquip ex ea commodo consequat. Duis aute ',
|
||||
'irure dolor in reprehenderit in voluptate velit esse ',
|
||||
'cillum dolore eu fugiat nulla pariatur...'
|
||||
].join('');
|
||||
TRUNCATED_TEXT = [
|
||||
'Adipisicing elit, sed do eiusmod tempor incididunt ',
|
||||
'ut labore et dolore magna aliqua. Ut enim ad minim ',
|
||||
'veniam, quis nostrud exercitation ullamco laboris ',
|
||||
'nisi ut aliquip ex ea commodo consequat. Duis aute ',
|
||||
'irure dolor in reprehenderit in voluptate velit esse ',
|
||||
'cillum dolore eu fugiat nulla pariatur. Exce'
|
||||
].join('');
|
||||
SHORT_TEXT = 'Adipisicing elit, sed do eiusmod tempor incididunt';
|
||||
|
||||
base64Encode = function (data) {
|
||||
var ac, bits, enc, h1, h2, h3, h4, i, o1, o2, o3, r, tmp_arr;
|
||||
if (btoa) {
|
||||
// Gecko and Webkit provide native code for this
|
||||
return btoa(data);
|
||||
} else {
|
||||
// Adapted from MIT/BSD licensed code at http://phpjs.org/functions/base64_encode
|
||||
// version 1109.2015
|
||||
i = 0;
|
||||
ac = 0;
|
||||
enc = "";
|
||||
tmp_arr = [];
|
||||
if (!data) {
|
||||
return data;
|
||||
}
|
||||
data += '';
|
||||
while (i < data.length) {
|
||||
o1 = data.charCodeAt(i++);
|
||||
o2 = data.charCodeAt(i++);
|
||||
o3 = data.charCodeAt(i++);
|
||||
bits = o1 << 16 | o2 << 8 | o3;
|
||||
h1 = bits >> 18 & 0x3f;
|
||||
h2 = bits >> 12 & 0x3f;
|
||||
h3 = bits >> 6 & 0x3f;
|
||||
h4 = bits & 0x3f;
|
||||
tmp_arr[ac++] = B64.charAt(h1) + B64.charAt(h2) + B64.charAt(h3) + B64.charAt(h4);
|
||||
}
|
||||
enc = tmp_arr.join('');
|
||||
r = data.length % 3;
|
||||
return (r ? enc.slice(0, r - 3) : enc) + '==='.slice(r || 3);
|
||||
}
|
||||
};
|
||||
|
||||
makeToken = function() {
|
||||
var now = (new Date()).getTime() / 1000,
|
||||
rawToken = {
|
||||
sub: "sub",
|
||||
exp: now + 100,
|
||||
iat: now
|
||||
};
|
||||
|
||||
return 'header.' + base64Encode(JSON.stringify(rawToken)) + '.signature';
|
||||
};
|
||||
getChapter = function (name, location, index, children) {
|
||||
return {
|
||||
display_name: name,
|
||||
location: 'i4x://chapter/' + location,
|
||||
index: index,
|
||||
children: _.map(children, function (i) {
|
||||
return 'i4x://section/' + i;
|
||||
})
|
||||
};
|
||||
};
|
||||
|
||||
getSection = function (name, location, children) {
|
||||
return {
|
||||
display_name: name,
|
||||
location: 'i4x://section/' + location,
|
||||
children: _.map(children, function (i) {
|
||||
return 'i4x://unit/' + i;
|
||||
})
|
||||
};
|
||||
};
|
||||
|
||||
getUnit = function (name, location) {
|
||||
return {
|
||||
display_name: name,
|
||||
location: 'i4x://unit/' + location,
|
||||
url: 'http://example.com'
|
||||
};
|
||||
};
|
||||
|
||||
getDefaultNotes = function () {
|
||||
return [
|
||||
{
|
||||
chapter: getChapter('Second Chapter', 0, 1, [1, 'w_n', 0]),
|
||||
section: getSection('Third Section', 0, ['w_n', 1, 0]),
|
||||
unit: getUnit('Fourth Unit', 0),
|
||||
created: 'December 11, 2014 at 11:12AM',
|
||||
updated: 'December 11, 2014 at 11:12AM',
|
||||
text: 'Third added model',
|
||||
quote: 'Note 4'
|
||||
},
|
||||
{
|
||||
chapter: getChapter('Second Chapter', 0, 1, [1, 'w_n', 0]),
|
||||
section: getSection('Third Section', 0, ['w_n', 1, 0]),
|
||||
unit: getUnit('Fourth Unit', 0),
|
||||
created: 'December 11, 2014 at 11:11AM',
|
||||
updated: 'December 11, 2014 at 11:11AM',
|
||||
text: 'Third added model',
|
||||
quote: 'Note 5'
|
||||
},
|
||||
{
|
||||
chapter: getChapter('Second Chapter', 0, 1, [1, 'w_n', 0]),
|
||||
section: getSection('Third Section', 0, ['w_n', 1, 0]),
|
||||
unit: getUnit('Third Unit', 1),
|
||||
created: 'December 11, 2014 at 11:11AM',
|
||||
updated: 'December 11, 2014 at 11:11AM',
|
||||
text: 'Second added model',
|
||||
quote: 'Note 3'
|
||||
},
|
||||
{
|
||||
chapter: getChapter('Second Chapter', 0, 1, [1, 'w_n', 0]),
|
||||
section: getSection('Second Section', 1, [2]),
|
||||
unit: getUnit('Second Unit', 2),
|
||||
created: 'December 11, 2014 at 11:10AM',
|
||||
updated: 'December 11, 2014 at 11:10AM',
|
||||
text: 'First added model',
|
||||
quote: 'Note 2'
|
||||
},
|
||||
{
|
||||
chapter: getChapter('First Chapter', 1, 0, [2]),
|
||||
section: getSection('First Section', 2, [3]),
|
||||
unit: getUnit('First Unit', 3),
|
||||
created: 'December 11, 2014 at 11:10AM',
|
||||
updated: 'December 11, 2014 at 11:10AM',
|
||||
text: 'First added model',
|
||||
quote: 'Note 1'
|
||||
}
|
||||
];
|
||||
};
|
||||
|
||||
return {
|
||||
LONG_TEXT: LONG_TEXT,
|
||||
PRUNED_TEXT: PRUNED_TEXT,
|
||||
TRUNCATED_TEXT: TRUNCATED_TEXT,
|
||||
SHORT_TEXT: SHORT_TEXT,
|
||||
base64Encode: base64Encode,
|
||||
makeToken: makeToken,
|
||||
getChapter: getChapter,
|
||||
getSection: getSection,
|
||||
getUnit: getUnit,
|
||||
getDefaultNotes: getDefaultNotes
|
||||
};
|
||||
});
|
||||
34
lms/static/js/spec/edxnotes/models/note_spec.js
Normal file
34
lms/static/js/spec/edxnotes/models/note_spec.js
Normal file
@@ -0,0 +1,34 @@
|
||||
define([
|
||||
'js/spec/edxnotes/helpers', 'js/edxnotes/collections/notes'
|
||||
], function(Helpers, NotesCollection) {
|
||||
'use strict';
|
||||
describe('EdxNotes NoteModel', function() {
|
||||
beforeEach(function () {
|
||||
this.collection = new NotesCollection([
|
||||
{quote: Helpers.LONG_TEXT},
|
||||
{quote: Helpers.SHORT_TEXT}
|
||||
]);
|
||||
});
|
||||
|
||||
it('has correct values on initialization', function () {
|
||||
expect(this.collection.at(0).get('is_expanded')).toBeFalsy();
|
||||
expect(this.collection.at(0).get('show_link')).toBeTruthy();
|
||||
expect(this.collection.at(1).get('is_expanded')).toBeFalsy();
|
||||
expect(this.collection.at(1).get('show_link')).toBeFalsy();
|
||||
});
|
||||
|
||||
it('can return appropriate note text', function () {
|
||||
var model = this.collection.at(0);
|
||||
|
||||
// is_expanded = false, show_link = true
|
||||
expect(model.getNoteText()).toBe(Helpers.PRUNED_TEXT);
|
||||
model.set('is_expanded', true);
|
||||
// is_expanded = true, show_link = true
|
||||
expect(model.getNoteText()).toBe(Helpers.LONG_TEXT);
|
||||
model.set('show_link', false);
|
||||
model.set('is_expanded', false);
|
||||
// is_expanded = false, show_link = false
|
||||
expect(model.getNoteText()).toBe(Helpers.LONG_TEXT);
|
||||
});
|
||||
});
|
||||
});
|
||||
33
lms/static/js/spec/edxnotes/models/tab_spec.js
Normal file
33
lms/static/js/spec/edxnotes/models/tab_spec.js
Normal file
@@ -0,0 +1,33 @@
|
||||
define([
|
||||
'js/edxnotes/collections/tabs'
|
||||
], function(TabsCollection) {
|
||||
'use strict';
|
||||
describe('EdxNotes TabModel', function() {
|
||||
beforeEach(function () {
|
||||
this.collection = new TabsCollection([{}, {}, {}]);
|
||||
});
|
||||
|
||||
it('when activate current model, all other models are inactivated', function () {
|
||||
this.collection.at(1).activate();
|
||||
expect(this.collection.at(1).get('is_active')).toBeTruthy();
|
||||
expect(this.collection.at(0).get('is_active')).toBeFalsy();
|
||||
expect(this.collection.at(2).get('is_active')).toBeFalsy();
|
||||
});
|
||||
|
||||
it('can inactivate current model', function () {
|
||||
var model = this.collection.at(0);
|
||||
model.activate();
|
||||
expect(model.get('is_active')).toBeTruthy();
|
||||
model.inactivate();
|
||||
expect(model.get('is_active')).toBeFalsy();
|
||||
});
|
||||
|
||||
it('can see correct activity status via isActive', function () {
|
||||
var model = this.collection.at(0);
|
||||
model.activate();
|
||||
expect(model.isActive()).toBeTruthy();
|
||||
model.inactivate();
|
||||
expect(model.isActive()).toBeFalsy();
|
||||
});
|
||||
});
|
||||
});
|
||||
158
lms/static/js/spec/edxnotes/plugins/events_spec.js
Normal file
158
lms/static/js/spec/edxnotes/plugins/events_spec.js
Normal file
@@ -0,0 +1,158 @@
|
||||
define([
|
||||
'jquery', 'underscore', 'js/common_helpers/ajax_helpers', 'js/spec/edxnotes/helpers',
|
||||
'annotator', 'logger', 'js/edxnotes/views/notes_factory'
|
||||
], function($, _, AjaxHelpers, Helpers, Annotator, Logger, NotesFactory) {
|
||||
'use strict';
|
||||
describe('EdxNotes Events Plugin', function() {
|
||||
var note = {
|
||||
user: 'user-123',
|
||||
id: 'note-123',
|
||||
text: 'text-123',
|
||||
quote: 'quote-123',
|
||||
usage_id: 'usage-123'
|
||||
},
|
||||
noteWithoutId = {
|
||||
user: 'user-123',
|
||||
text: 'text-123',
|
||||
quote: 'quote-123',
|
||||
usage_id: 'usage-123'
|
||||
};
|
||||
|
||||
beforeEach(function() {
|
||||
this.annotator = NotesFactory.factory(
|
||||
$('<div />').get(0), {
|
||||
endpoint: 'http://example.com/'
|
||||
}
|
||||
);
|
||||
spyOn(Logger, 'log');
|
||||
});
|
||||
|
||||
afterEach(function () {
|
||||
_.invoke(Annotator._instances, 'destroy');
|
||||
});
|
||||
|
||||
it('should log edx.course.student_notes.viewed event properly', function() {
|
||||
this.annotator.publish('annotationViewerShown', [
|
||||
this.annotator.viewer,
|
||||
[note, {user: 'user-456'}, {user: 'user-789', id: 'note-789'}]
|
||||
]);
|
||||
expect(Logger.log).toHaveBeenCalledWith(
|
||||
'edx.course.student_notes.viewed', {
|
||||
'notes': [{'note_id': 'note-123'}, {'note_id': 'note-789'}]
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
it('should not log edx.course.student_notes.viewed event if all notes are new', function() {
|
||||
this.annotator.publish('annotationViewerShown', [
|
||||
this.annotator.viewer, [{user: 'user-456'}, {user: 'user-789'}]
|
||||
]);
|
||||
expect(Logger.log).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should log edx.course.student_notes.added event properly', function() {
|
||||
var requests = AjaxHelpers.requests(this),
|
||||
newNote = {
|
||||
user: 'user-123',
|
||||
text: 'text-123',
|
||||
quote: 'quote-123',
|
||||
usage_id: 'usage-123'
|
||||
};
|
||||
|
||||
this.annotator.publish('annotationCreated', newNote);
|
||||
AjaxHelpers.respondWithJson(requests, note);
|
||||
expect(Logger.log).toHaveBeenCalledWith(
|
||||
'edx.course.student_notes.added', {
|
||||
'note_id': 'note-123',
|
||||
'note_text': 'text-123',
|
||||
'note_text_truncated': false,
|
||||
'highlighted_content': 'quote-123',
|
||||
'highlighted_content_truncated': false,
|
||||
'component_usage_id': 'usage-123'
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
it('should log the edx.course.student_notes.edited event properly', function() {
|
||||
var oldNote = note,
|
||||
newNote = $.extend({}, note, {text: 'text-456'});
|
||||
|
||||
this.annotator.publish('annotationEditorShown', [this.annotator.editor, oldNote]);
|
||||
expect(this.annotator.plugins.Events.oldNoteText).toBe('text-123');
|
||||
this.annotator.publish('annotationUpdated', newNote);
|
||||
this.annotator.publish('annotationEditorHidden', [this.annotator.editor, newNote]);
|
||||
|
||||
expect(Logger.log).toHaveBeenCalledWith(
|
||||
'edx.course.student_notes.edited', {
|
||||
'note_id': 'note-123',
|
||||
'old_note_text': 'text-123',
|
||||
'old_note_text_truncated': false,
|
||||
'note_text': 'text-456',
|
||||
'note_text_truncated': false,
|
||||
'highlighted_content': 'quote-123',
|
||||
'highlighted_content_truncated': false,
|
||||
'component_usage_id': 'usage-123'
|
||||
}
|
||||
);
|
||||
expect(this.annotator.plugins.Events.oldNoteText).toBeNull();
|
||||
});
|
||||
|
||||
it('should not log the edx.course.student_notes.edited event if the note is new', function() {
|
||||
var oldNote = noteWithoutId,
|
||||
newNote = $.extend({}, noteWithoutId, {text: 'text-456'});
|
||||
|
||||
this.annotator.publish('annotationEditorShown', [this.annotator.editor, oldNote]);
|
||||
expect(this.annotator.plugins.Events.oldNoteText).toBe('text-123');
|
||||
this.annotator.publish('annotationUpdated', newNote);
|
||||
this.annotator.publish('annotationEditorHidden', [this.annotator.editor, newNote]);
|
||||
expect(Logger.log).not.toHaveBeenCalled();
|
||||
expect(this.annotator.plugins.Events.oldNoteText).toBeNull();
|
||||
});
|
||||
|
||||
it('should log the edx.course.student_notes.deleted event properly', function() {
|
||||
this.annotator.publish('annotationDeleted', note);
|
||||
expect(Logger.log).toHaveBeenCalledWith(
|
||||
'edx.course.student_notes.deleted', {
|
||||
'note_id': 'note-123',
|
||||
'note_text': 'text-123',
|
||||
'note_text_truncated': false,
|
||||
'highlighted_content': 'quote-123',
|
||||
'highlighted_content_truncated': false,
|
||||
'component_usage_id': 'usage-123'
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
it('should not log the edx.course.student_notes.deleted event if the note is new', function() {
|
||||
this.annotator.publish('annotationDeleted', noteWithoutId);
|
||||
expect(Logger.log).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should truncate values of some fields', function() {
|
||||
var oldNote = $.extend({}, note, {text: Helpers.LONG_TEXT}),
|
||||
newNote = $.extend({}, note, {
|
||||
text: Helpers.LONG_TEXT + '123',
|
||||
quote: Helpers.LONG_TEXT + '123'
|
||||
});
|
||||
|
||||
this.annotator.publish('annotationEditorShown', [this.annotator.editor, oldNote]);
|
||||
expect(this.annotator.plugins.Events.oldNoteText).toBe(Helpers.LONG_TEXT);
|
||||
this.annotator.publish('annotationUpdated', newNote);
|
||||
this.annotator.publish('annotationEditorHidden', [this.annotator.editor, newNote]);
|
||||
|
||||
expect(Logger.log).toHaveBeenCalledWith(
|
||||
'edx.course.student_notes.edited', {
|
||||
'note_id': 'note-123',
|
||||
'old_note_text': Helpers.TRUNCATED_TEXT,
|
||||
'old_note_text_truncated': true,
|
||||
'note_text': Helpers.TRUNCATED_TEXT,
|
||||
'note_text_truncated': true,
|
||||
'highlighted_content': Helpers.TRUNCATED_TEXT,
|
||||
'highlighted_content_truncated': true,
|
||||
'component_usage_id': 'usage-123'
|
||||
}
|
||||
);
|
||||
expect(this.annotator.plugins.Events.oldNoteText).toBeNull();
|
||||
});
|
||||
});
|
||||
});
|
||||
94
lms/static/js/spec/edxnotes/plugins/scroller_spec.js
Normal file
94
lms/static/js/spec/edxnotes/plugins/scroller_spec.js
Normal file
@@ -0,0 +1,94 @@
|
||||
define([
|
||||
'jquery', 'underscore', 'annotator', 'js/edxnotes/views/notes_factory',
|
||||
'js/spec/edxnotes/custom_matchers'
|
||||
], function($, _, Annotator, NotesFactory, customMatchers) {
|
||||
'use strict';
|
||||
describe('EdxNotes Scroll Plugin', function() {
|
||||
var annotators, highlights;
|
||||
|
||||
function checkAnnotatorIsFrozen(annotator) {
|
||||
expect(annotator.isFrozen).toBe(true);
|
||||
expect(annotator.onHighlightMouseover).not.toHaveBeenCalled();
|
||||
expect(annotator.startViewerHideTimer).not.toHaveBeenCalled();
|
||||
}
|
||||
|
||||
function checkAnnotatorIsUnfrozen(annotator) {
|
||||
expect(annotator.isFrozen).toBe(false);
|
||||
expect(annotator.onHighlightMouseover).toHaveBeenCalled();
|
||||
expect(annotator.startViewerHideTimer).toHaveBeenCalled();
|
||||
}
|
||||
|
||||
beforeEach(function() {
|
||||
customMatchers(this);
|
||||
loadFixtures('js/fixtures/edxnotes/edxnotes_wrapper.html');
|
||||
annotators = [
|
||||
NotesFactory.factory($('div#edx-notes-wrapper-123').get(0), {
|
||||
endpoint: 'http://example.com/'
|
||||
}),
|
||||
NotesFactory.factory($('div#edx-notes-wrapper-456').get(0), {
|
||||
endpoint: 'http://example.com/'
|
||||
})
|
||||
];
|
||||
|
||||
highlights = _.map(annotators, function(annotator) {
|
||||
spyOn(annotator, 'onHighlightClick').andCallThrough();
|
||||
spyOn(annotator, 'onHighlightMouseover').andCallThrough();
|
||||
spyOn(annotator, 'startViewerHideTimer').andCallThrough();
|
||||
return $('<span></span>', {
|
||||
'class': 'annotator-hl',
|
||||
'tabindex': -1,
|
||||
'text': 'some content'
|
||||
}).appendTo(annotator.element);
|
||||
});
|
||||
|
||||
spyOn(annotators[0].plugins.Scroller, 'getIdFromLocationHash').andReturn('abc123');
|
||||
spyOn($.fn, 'unbind').andCallThrough();
|
||||
});
|
||||
|
||||
afterEach(function () {
|
||||
_.invoke(Annotator._instances, 'destroy');
|
||||
});
|
||||
|
||||
it('should scroll to a note, open it and freeze the annotator if its id is part of the url hash', function() {
|
||||
annotators[0].plugins.Scroller.onNotesLoaded([{
|
||||
id: 'abc123',
|
||||
highlights: [highlights[0]]
|
||||
}]);
|
||||
annotators[0].onHighlightMouseover.reset();
|
||||
expect(highlights[0]).toBeFocused();
|
||||
highlights[0].mouseover();
|
||||
highlights[0].mouseout();
|
||||
checkAnnotatorIsFrozen(annotators[0]);
|
||||
});
|
||||
|
||||
it('should not do anything if the url hash contains a wrong id', function() {
|
||||
annotators[0].plugins.Scroller.onNotesLoaded([{
|
||||
id: 'def456',
|
||||
highlights: [highlights[0]]
|
||||
}]);
|
||||
expect(highlights[0]).not.toBeFocused();
|
||||
highlights[0].mouseover();
|
||||
highlights[0].mouseout();
|
||||
checkAnnotatorIsUnfrozen(annotators[0]);
|
||||
});
|
||||
|
||||
it('should not do anything if the url hash contains an empty id', function() {
|
||||
annotators[0].plugins.Scroller.onNotesLoaded([{
|
||||
id: '',
|
||||
highlights: [highlights[0]]
|
||||
}]);
|
||||
expect(highlights[0]).not.toBeFocused();
|
||||
highlights[0].mouseover();
|
||||
highlights[0].mouseout();
|
||||
checkAnnotatorIsUnfrozen(annotators[0]);
|
||||
});
|
||||
|
||||
it('should unbind onNotesLoaded on destruction', function() {
|
||||
annotators[0].plugins.Scroller.destroy();
|
||||
expect($.fn.unbind).toHaveBeenCalledWith(
|
||||
'annotationsLoaded',
|
||||
annotators[0].plugins.Scroller.onNotesLoaded
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
124
lms/static/js/spec/edxnotes/utils/logger_spec.js
Normal file
124
lms/static/js/spec/edxnotes/utils/logger_spec.js
Normal file
@@ -0,0 +1,124 @@
|
||||
define([
|
||||
'logger', 'js/edxnotes/utils/logger', 'js/spec/edxnotes/custom_matchers'
|
||||
], function(Logger, NotesLogger, customMatchers) {
|
||||
'use strict';
|
||||
describe('Edxnotes NotesLogger', function() {
|
||||
var getLogger = function(id, mode) {
|
||||
return NotesLogger.getLogger(id, mode);
|
||||
};
|
||||
|
||||
beforeEach(function () {
|
||||
spyOn(window.console, 'log');
|
||||
spyOn(window.console, 'error');
|
||||
spyOn(Logger, 'log');
|
||||
customMatchers(this);
|
||||
});
|
||||
|
||||
it('keeps a correct history of logs', function() {
|
||||
var logger = getLogger('id', 1),
|
||||
logs, log;
|
||||
|
||||
logger.log('A log type', 'A first log');
|
||||
logger.log('A log type', 'A second log');
|
||||
expect(window.console.log).toHaveBeenCalled();
|
||||
|
||||
logs = logger.getHistory();
|
||||
// Test first log
|
||||
log = logs[0];
|
||||
expect(log[0]).toBe('log');
|
||||
expect(log[1][0]).toBe('id');
|
||||
expect(log[1][1]).toBe('A log type');
|
||||
expect(log[1][2]).toBe('A first log');
|
||||
|
||||
// Test second log
|
||||
log = logs[1];
|
||||
expect(log[0]).toBe('log');
|
||||
expect(log[1][0]).toBe('id');
|
||||
expect(log[1][1]).toBe('A log type');
|
||||
expect(log[1][2]).toBe('A second log');
|
||||
});
|
||||
|
||||
it('keeps a correct history of errors', function() {
|
||||
var logger = getLogger('id', 1),
|
||||
logs, log;
|
||||
logger.error('An error type', 'A first error');
|
||||
logger.error('An error type', 'A second error');
|
||||
expect(window.console.error).toHaveBeenCalled();
|
||||
|
||||
logs = logger.getHistory();
|
||||
// Test first error
|
||||
log = logs[0];
|
||||
expect(log[0]).toBe('error');
|
||||
expect(log[1][0]).toBe('id');
|
||||
expect(log[1][1]).toBe('An error type');
|
||||
expect(log[1][2]).toBe('A first error');
|
||||
|
||||
// Test second error
|
||||
log = logs[1];
|
||||
expect(log[0]).toBe('error');
|
||||
expect(log[1][0]).toBe('id');
|
||||
expect(log[1][1]).toBe('An error type');
|
||||
expect(log[1][2]).toBe('A second error');
|
||||
});
|
||||
|
||||
it('can destroy the logger', function() {
|
||||
var logger = getLogger('id', 1),
|
||||
logs;
|
||||
|
||||
logger.log('A log type', 'A first log');
|
||||
logger.error('An error type', 'A first error');
|
||||
logs = logger.getHistory();
|
||||
expect(logs.length).toBe(2);
|
||||
logger.destroy();
|
||||
logs = logger.getHistory();
|
||||
expect(logs.length).toBe(0);
|
||||
});
|
||||
|
||||
it('do not store the history in silent mode', function() {
|
||||
var logger = getLogger('id', 0),
|
||||
logs;
|
||||
logger.log('A log type', 'A first log');
|
||||
logger.error('An error type', 'A first error');
|
||||
logs = logger.getHistory();
|
||||
expect(logs.length).toBe(0);
|
||||
});
|
||||
|
||||
it('do not show logs in the console in silent mode', function() {
|
||||
var logger = getLogger('id', 0);
|
||||
logger.log('A log type', 'A first log');
|
||||
logger.error('An error type', 'A first error');
|
||||
expect(window.console.log).not.toHaveBeenCalled();
|
||||
expect(window.console.error).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('can use timers', function() {
|
||||
var logger = getLogger('id', 1),
|
||||
now, t0, logs, log;
|
||||
|
||||
now = function () {
|
||||
return (new Date()).getTime();
|
||||
};
|
||||
|
||||
t0 = now();
|
||||
logger.time('timer');
|
||||
while (now() - t0 < 200) {}
|
||||
logger.timeEnd('timer');
|
||||
|
||||
logs = logger.getHistory();
|
||||
log = logs[0];
|
||||
expect(log[0]).toBe('log');
|
||||
expect(log[1][0]).toBe('id');
|
||||
expect(log[1][1]).toBe('timer');
|
||||
expect(log[1][2]).toBeInRange(180, 220);
|
||||
expect(log[1][3]).toBe('ms');
|
||||
});
|
||||
|
||||
it('can emit an event properly', function () {
|
||||
var logger = getLogger('id', 0);
|
||||
logger.emit('event_name', {id: 'some_id'})
|
||||
expect(Logger.log).toHaveBeenCalledWith('event_name', {
|
||||
id: 'some_id'
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
81
lms/static/js/spec/edxnotes/views/note_item_spec.js
Normal file
81
lms/static/js/spec/edxnotes/views/note_item_spec.js
Normal file
@@ -0,0 +1,81 @@
|
||||
define([
|
||||
'jquery', 'underscore', 'js/common_helpers/ajax_helpers',
|
||||
'js/common_helpers/template_helpers', 'js/spec/edxnotes/helpers', 'logger',
|
||||
'js/edxnotes/models/note', 'js/edxnotes/views/note_item',
|
||||
'js/spec/edxnotes/custom_matchers'
|
||||
], function(
|
||||
$, _, AjaxHelpers, TemplateHelpers, Helpers, Logger, NoteModel, NoteItemView,
|
||||
customMatchers
|
||||
) {
|
||||
'use strict';
|
||||
describe('EdxNotes NoteItemView', function() {
|
||||
var getView = function (model) {
|
||||
model = new NoteModel(_.defaults(model || {}, {
|
||||
id: 'id-123',
|
||||
user: 'user-123',
|
||||
usage_id: 'usage_id-123',
|
||||
created: 'December 11, 2014 at 11:12AM',
|
||||
updated: 'December 11, 2014 at 11:12AM',
|
||||
text: 'Third added model',
|
||||
quote: Helpers.LONG_TEXT,
|
||||
unit: {
|
||||
url: 'http://example.com/'
|
||||
}
|
||||
}));
|
||||
|
||||
return new NoteItemView({model: model}).render();
|
||||
};
|
||||
|
||||
beforeEach(function() {
|
||||
customMatchers(this);
|
||||
TemplateHelpers.installTemplate('templates/edxnotes/note-item');
|
||||
spyOn(Logger, 'log').andCallThrough();
|
||||
});
|
||||
|
||||
it('can be rendered properly', function() {
|
||||
var view = getView(),
|
||||
unitLink = view.$('.reference-unit-link').get(0);
|
||||
|
||||
expect(view.$el).toContain('.note-excerpt-more-link');
|
||||
expect(view.$el).toContainText(Helpers.PRUNED_TEXT);
|
||||
expect(view.$el).toContainText('More');
|
||||
view.$('.note-excerpt-more-link').click();
|
||||
|
||||
expect(view.$el).toContainText(Helpers.LONG_TEXT);
|
||||
expect(view.$el).toContainText('Less');
|
||||
|
||||
view = getView({quote: Helpers.SHORT_TEXT});
|
||||
expect(view.$el).not.toContain('.note-excerpt-more-link');
|
||||
expect(view.$el).toContainText(Helpers.SHORT_TEXT);
|
||||
|
||||
expect(unitLink.hash).toBe('#id-123');
|
||||
});
|
||||
|
||||
it('should display update value and accompanying text', function() {
|
||||
var view = getView();
|
||||
expect(view.$('.reference-title').last()).toContainText('Last Edited:');
|
||||
expect(view.$('.reference-meta').last()).toContainText('December 11, 2014 at 11:12AM');
|
||||
});
|
||||
|
||||
it('should log the edx.student_notes.used_unit_link event properly', function () {
|
||||
var requests = AjaxHelpers.requests(this),
|
||||
view = getView();
|
||||
spyOn(view, 'redirectTo');
|
||||
view.$('.reference-unit-link').click();
|
||||
expect(Logger.log).toHaveBeenCalledWith(
|
||||
'edx.student_notes.used_unit_link',
|
||||
{
|
||||
'note_id': 'id-123',
|
||||
'component_usage_id': 'usage_id-123'
|
||||
},
|
||||
null,
|
||||
{
|
||||
'timeout': 2000
|
||||
}
|
||||
);
|
||||
expect(view.redirectTo).not.toHaveBeenCalled();
|
||||
AjaxHelpers.respondWithJson(requests, {});
|
||||
expect(view.redirectTo).toHaveBeenCalledWith('http://example.com/#id-123');
|
||||
});
|
||||
});
|
||||
});
|
||||
43
lms/static/js/spec/edxnotes/views/notes_factory_spec.js
Normal file
43
lms/static/js/spec/edxnotes/views/notes_factory_spec.js
Normal file
@@ -0,0 +1,43 @@
|
||||
define([
|
||||
'annotator', 'js/edxnotes/views/notes_factory', 'js/common_helpers/ajax_helpers',
|
||||
'js/spec/edxnotes/helpers', 'js/spec/edxnotes/custom_matchers'
|
||||
], function(Annotator, NotesFactory, AjaxHelpers, Helpers, customMatchers) {
|
||||
'use strict';
|
||||
describe('EdxNotes NotesFactory', function() {
|
||||
beforeEach(function() {
|
||||
customMatchers(this);
|
||||
loadFixtures('js/fixtures/edxnotes/edxnotes_wrapper.html');
|
||||
this.wrapper = document.getElementById('edx-notes-wrapper-123');
|
||||
});
|
||||
|
||||
afterEach(function () {
|
||||
_.invoke(Annotator._instances, 'destroy');
|
||||
});
|
||||
|
||||
it('can initialize annotator correctly', function() {
|
||||
var requests = AjaxHelpers.requests(this),
|
||||
token = Helpers.makeToken(),
|
||||
options = {
|
||||
user: 'a user',
|
||||
usage_id : 'an usage',
|
||||
course_id: 'a course'
|
||||
},
|
||||
annotator = NotesFactory.factory(this.wrapper, {
|
||||
endpoint: '/test_endpoint',
|
||||
user: 'a user',
|
||||
usageId : 'an usage',
|
||||
courseId: 'a course',
|
||||
token: token,
|
||||
tokenUrl: '/test_token_url'
|
||||
}),
|
||||
request = requests[0];
|
||||
|
||||
expect(requests).toHaveLength(1);
|
||||
expect(request.requestHeaders['x-annotator-auth-token']).toBe(token);
|
||||
expect(annotator.options.auth.tokenUrl).toBe('/test_token_url');
|
||||
expect(annotator.options.store.prefix).toBe('/test_endpoint');
|
||||
expect(annotator.options.store.annotationData).toEqual(options);
|
||||
expect(annotator.options.store.loadFromSearch).toEqual(options);
|
||||
});
|
||||
});
|
||||
});
|
||||
46
lms/static/js/spec/edxnotes/views/notes_page_spec.js
Normal file
46
lms/static/js/spec/edxnotes/views/notes_page_spec.js
Normal file
@@ -0,0 +1,46 @@
|
||||
define([
|
||||
'jquery', 'underscore', 'js/common_helpers/template_helpers',
|
||||
'js/common_helpers/ajax_helpers', 'js/spec/edxnotes/helpers',
|
||||
'js/edxnotes/views/page_factory', 'js/spec/edxnotes/custom_matchers'
|
||||
], function($, _, TemplateHelpers, AjaxHelpers, Helpers, NotesFactory, customMatchers) {
|
||||
'use strict';
|
||||
describe('EdxNotes NotesPage', function() {
|
||||
var notes = Helpers.getDefaultNotes();
|
||||
|
||||
beforeEach(function() {
|
||||
customMatchers(this);
|
||||
loadFixtures('js/fixtures/edxnotes/edxnotes.html');
|
||||
TemplateHelpers.installTemplates([
|
||||
'templates/edxnotes/note-item', 'templates/edxnotes/tab-item'
|
||||
]);
|
||||
this.view = new NotesFactory({notesList: notes});
|
||||
});
|
||||
|
||||
|
||||
it('should be displayed properly', function() {
|
||||
var requests = AjaxHelpers.requests(this),
|
||||
tab;
|
||||
|
||||
expect(this.view.$('#view-search-results')).not.toExist();
|
||||
tab = this.view.$('#view-recent-activity');
|
||||
expect(tab).toHaveClass('is-active');
|
||||
expect(tab.index()).toBe(0);
|
||||
|
||||
tab = this.view.$('#view-course-structure');
|
||||
expect(tab).toExist();
|
||||
expect(tab.index()).toBe(1);
|
||||
|
||||
expect(this.view.$('.tab-panel')).toExist();
|
||||
|
||||
this.view.$('.search-notes-input').val('test_query');
|
||||
this.view.$('.search-notes-submit').click();
|
||||
AjaxHelpers.respondWithJson(requests, {
|
||||
total: 0,
|
||||
rows: []
|
||||
});
|
||||
expect(this.view.$('#view-search-results')).toHaveClass('is-active');
|
||||
expect(this.view.$('#view-recent-activity')).toExist();
|
||||
expect(this.view.$('#view-course-structure')).toExist();
|
||||
});
|
||||
});
|
||||
});
|
||||
162
lms/static/js/spec/edxnotes/views/search_box_spec.js
Normal file
162
lms/static/js/spec/edxnotes/views/search_box_spec.js
Normal file
@@ -0,0 +1,162 @@
|
||||
define([
|
||||
'jquery', 'underscore', 'js/common_helpers/ajax_helpers', 'js/edxnotes/views/search_box',
|
||||
'js/edxnotes/collections/notes', 'js/spec/edxnotes/custom_matchers', 'jasmine-jquery'
|
||||
], function($, _, AjaxHelpers, SearchBoxView, NotesCollection, customMatchers) {
|
||||
'use strict';
|
||||
describe('EdxNotes SearchBoxView', function() {
|
||||
var getSearchBox, submitForm, assertBoxIsEnabled, assertBoxIsDisabled;
|
||||
|
||||
getSearchBox = function (options) {
|
||||
options = _.defaults(options || {}, {
|
||||
el: $('#search-notes-form').get(0),
|
||||
beforeSearchStart: jasmine.createSpy(),
|
||||
search: jasmine.createSpy(),
|
||||
error: jasmine.createSpy(),
|
||||
complete: jasmine.createSpy()
|
||||
});
|
||||
|
||||
return new SearchBoxView(options);
|
||||
};
|
||||
|
||||
submitForm = function (searchBox, text) {
|
||||
searchBox.$('.search-notes-input').val(text);
|
||||
searchBox.$('.search-notes-submit').click();
|
||||
};
|
||||
|
||||
assertBoxIsEnabled = function (searchBox) {
|
||||
expect(searchBox.$el).not.toHaveClass('is-looking');
|
||||
expect(searchBox.$('.search-notes-submit')).not.toHaveClass('is-disabled');
|
||||
expect(searchBox.isDisabled).toBeFalsy();
|
||||
};
|
||||
|
||||
assertBoxIsDisabled = function (searchBox) {
|
||||
expect(searchBox.$el).toHaveClass('is-looking');
|
||||
expect(searchBox.$('.search-notes-submit')).toHaveClass('is-disabled');
|
||||
expect(searchBox.isDisabled).toBeTruthy();
|
||||
};
|
||||
|
||||
beforeEach(function () {
|
||||
customMatchers(this);
|
||||
loadFixtures('js/fixtures/edxnotes/edxnotes.html');
|
||||
spyOn(Logger, 'log');
|
||||
this.searchBox = getSearchBox();
|
||||
});
|
||||
|
||||
it('sends a request with proper information on submit the form', function () {
|
||||
var requests = AjaxHelpers.requests(this),
|
||||
form = this.searchBox.el,
|
||||
request;
|
||||
|
||||
submitForm(this.searchBox, 'test_text');
|
||||
request = requests[0];
|
||||
expect(request.method).toBe(form.method.toUpperCase());
|
||||
expect(request.url).toBe(form.action + '?' + $.param({text: 'test_text'}));
|
||||
});
|
||||
|
||||
it('returns success result', function () {
|
||||
var requests = AjaxHelpers.requests(this);
|
||||
submitForm(this.searchBox, 'test_text');
|
||||
expect(this.searchBox.options.beforeSearchStart).toHaveBeenCalledWith(
|
||||
'test_text'
|
||||
);
|
||||
assertBoxIsDisabled(this.searchBox);
|
||||
AjaxHelpers.respondWithJson(requests, {
|
||||
total: 2,
|
||||
rows: [null, null]
|
||||
});
|
||||
assertBoxIsEnabled(this.searchBox);
|
||||
expect(this.searchBox.options.search).toHaveBeenCalledWith(
|
||||
jasmine.any(NotesCollection), 2, 'test_text'
|
||||
);
|
||||
expect(this.searchBox.options.complete).toHaveBeenCalledWith(
|
||||
'test_text'
|
||||
);
|
||||
});
|
||||
|
||||
it('should log the edx.student_notes.searched event properly', function () {
|
||||
var requests = AjaxHelpers.requests(this);
|
||||
submitForm(this.searchBox, 'test_text');
|
||||
AjaxHelpers.respondWithJson(requests, {
|
||||
total: 2,
|
||||
rows: [null, null]
|
||||
});
|
||||
|
||||
expect(Logger.log).toHaveBeenCalledWith('edx.student_notes.searched', {
|
||||
'number_of_results': 2,
|
||||
'search_string': 'test_text'
|
||||
});
|
||||
});
|
||||
|
||||
it('returns default error message if received data structure is wrong', function () {
|
||||
var requests = AjaxHelpers.requests(this);
|
||||
submitForm(this.searchBox, 'test_text');
|
||||
AjaxHelpers.respondWithJson(requests, {});
|
||||
expect(this.searchBox.options.error).toHaveBeenCalledWith(
|
||||
'An error has occurred. Make sure that you are connected to the Internet, and then try refreshing the page.',
|
||||
'test_text'
|
||||
);
|
||||
expect(this.searchBox.options.complete).toHaveBeenCalledWith(
|
||||
'test_text'
|
||||
);
|
||||
});
|
||||
|
||||
it('returns default error message if network error occurs', function () {
|
||||
var requests = AjaxHelpers.requests(this);
|
||||
submitForm(this.searchBox, 'test_text');
|
||||
AjaxHelpers.respondWithError(requests);
|
||||
expect(this.searchBox.options.error).toHaveBeenCalledWith(
|
||||
'An error has occurred. Make sure that you are connected to the Internet, and then try refreshing the page.',
|
||||
'test_text'
|
||||
);
|
||||
expect(this.searchBox.options.complete).toHaveBeenCalledWith(
|
||||
'test_text'
|
||||
);
|
||||
});
|
||||
|
||||
it('returns error message if server error occurs', function () {
|
||||
var requests = AjaxHelpers.requests(this);
|
||||
submitForm(this.searchBox, 'test_text');
|
||||
assertBoxIsDisabled(this.searchBox);
|
||||
|
||||
requests[0].respond(
|
||||
500, {'Content-Type': 'application/json'},
|
||||
JSON.stringify({
|
||||
error: 'test error message'
|
||||
})
|
||||
);
|
||||
|
||||
assertBoxIsEnabled(this.searchBox);
|
||||
expect(this.searchBox.options.error).toHaveBeenCalledWith(
|
||||
'test error message',
|
||||
'test_text'
|
||||
);
|
||||
expect(this.searchBox.options.complete).toHaveBeenCalledWith(
|
||||
'test_text'
|
||||
);
|
||||
});
|
||||
|
||||
it('does not send second request during current search', function () {
|
||||
var requests = AjaxHelpers.requests(this);
|
||||
submitForm(this.searchBox, 'test_text');
|
||||
assertBoxIsDisabled(this.searchBox);
|
||||
submitForm(this.searchBox, 'another_text');
|
||||
AjaxHelpers.respondWithJson(requests, {
|
||||
total: 2,
|
||||
rows: [null, null]
|
||||
});
|
||||
assertBoxIsEnabled(this.searchBox);
|
||||
expect(requests).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('returns error message if the field is empty', function () {
|
||||
var requests = AjaxHelpers.requests(this);
|
||||
submitForm(this.searchBox, ' ');
|
||||
expect(requests).toHaveLength(0);
|
||||
assertBoxIsEnabled(this.searchBox);
|
||||
expect(this.searchBox.options.error).toHaveBeenCalledWith(
|
||||
'Please enter a term in the <a href="#search-notes-input"> search field</a>.',
|
||||
' '
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
125
lms/static/js/spec/edxnotes/views/shim_spec.js
Normal file
125
lms/static/js/spec/edxnotes/views/shim_spec.js
Normal file
@@ -0,0 +1,125 @@
|
||||
define([
|
||||
'jquery', 'underscore', 'annotator', 'js/edxnotes/views/notes_factory', 'jasmine-jquery'
|
||||
], function($, _, Annotator, NotesFactory) {
|
||||
'use strict';
|
||||
describe('EdxNotes Shim', function() {
|
||||
var annotators, highlights;
|
||||
|
||||
function checkAnnotatorIsFrozen(annotator) {
|
||||
expect(annotator.isFrozen).toBe(true);
|
||||
expect(annotator.onHighlightMouseover).not.toHaveBeenCalled();
|
||||
expect(annotator.startViewerHideTimer).not.toHaveBeenCalled();
|
||||
}
|
||||
|
||||
function checkAnnotatorIsUnfrozen(annotator) {
|
||||
expect(annotator.isFrozen).toBe(false);
|
||||
expect(annotator.onHighlightMouseover).toHaveBeenCalled();
|
||||
expect(annotator.startViewerHideTimer).toHaveBeenCalled();
|
||||
}
|
||||
|
||||
function checkClickEventsNotBound(namespace) {
|
||||
var events = $._data(document, 'events').click;
|
||||
|
||||
_.each(events, function(event) {
|
||||
expect(event.namespace.indexOf(namespace)).toBe(-1);
|
||||
});
|
||||
}
|
||||
|
||||
beforeEach(function() {
|
||||
loadFixtures('js/fixtures/edxnotes/edxnotes_wrapper.html');
|
||||
highlights = [];
|
||||
annotators = [
|
||||
NotesFactory.factory($('div#edx-notes-wrapper-123').get(0), {
|
||||
endpoint: 'http://example.com/'
|
||||
}),
|
||||
NotesFactory.factory($('div#edx-notes-wrapper-456').get(0), {
|
||||
endpoint: 'http://example.com/'
|
||||
})
|
||||
];
|
||||
_.each(annotators, function(annotator, index) {
|
||||
highlights.push($('<span class="annotator-hl" />').appendTo(annotators[index].element));
|
||||
spyOn(annotator, 'onHighlightClick').andCallThrough();
|
||||
spyOn(annotator, 'onHighlightMouseover').andCallThrough();
|
||||
spyOn(annotator, 'startViewerHideTimer').andCallThrough();
|
||||
});
|
||||
spyOn($.fn, 'off').andCallThrough();
|
||||
});
|
||||
|
||||
afterEach(function () {
|
||||
_.invoke(Annotator._instances, 'destroy');
|
||||
});
|
||||
|
||||
it('clicking a highlight freezes mouseover and mouseout in all highlighted text', function() {
|
||||
_.each(annotators, function(annotator) {
|
||||
expect(annotator.isFrozen).toBe(false);
|
||||
});
|
||||
highlights[0].click();
|
||||
// Click is attached to the onHighlightClick event handler which
|
||||
// in turn calls onHighlightMouseover.
|
||||
// To test if onHighlightMouseover is called or not on
|
||||
// mouseover, we'll have to reset onHighlightMouseover.
|
||||
expect(annotators[0].onHighlightClick).toHaveBeenCalled();
|
||||
expect(annotators[0].onHighlightMouseover).toHaveBeenCalled();
|
||||
annotators[0].onHighlightMouseover.reset();
|
||||
|
||||
// Check that both instances of annotator are frozen
|
||||
_.invoke(highlights, 'mouseover');
|
||||
_.invoke(highlights, 'mouseout');
|
||||
_.each(annotators, checkAnnotatorIsFrozen);
|
||||
});
|
||||
|
||||
it('clicking twice reverts to default behavior', function() {
|
||||
highlights[0].click();
|
||||
$(document).click();
|
||||
annotators[0].onHighlightMouseover.reset();
|
||||
|
||||
// Check that both instances of annotator are unfrozen
|
||||
_.invoke(highlights, 'mouseover');
|
||||
_.invoke(highlights, 'mouseout');
|
||||
_.each(annotators, function(annotator) {
|
||||
checkAnnotatorIsUnfrozen(annotator);
|
||||
});
|
||||
});
|
||||
|
||||
it('destroying an instance with an open viewer sets all other instances' +
|
||||
'to unfrozen and unbinds document click.edxnotes:freeze event handlers', function() {
|
||||
// Freeze all instances
|
||||
highlights[0].click();
|
||||
// Destroy first instance
|
||||
annotators[0].destroy();
|
||||
|
||||
// Check that all click.edxnotes:freeze are unbound
|
||||
checkClickEventsNotBound('edxnotes:freeze');
|
||||
|
||||
// Check that the remaining instance is unfrozen
|
||||
highlights[1].mouseover();
|
||||
highlights[1].mouseout();
|
||||
checkAnnotatorIsUnfrozen(annotators[1]);
|
||||
});
|
||||
|
||||
it('destroying an instance with an closed viewer only unfreezes that instance' +
|
||||
'and unbinds one document click.edxnotes:freeze event handlers', function() {
|
||||
// Freeze all instances
|
||||
highlights[0].click();
|
||||
annotators[0].onHighlightMouseover.reset();
|
||||
// Destroy second instance
|
||||
annotators[1].destroy();
|
||||
|
||||
// Check that the first instance is frozen
|
||||
highlights[0].mouseover();
|
||||
highlights[0].mouseout();
|
||||
checkAnnotatorIsFrozen(annotators[0]);
|
||||
|
||||
// Check that second one doesn't have a bound click.edxnotes:freeze
|
||||
checkClickEventsNotBound('edxnotes:freeze' + annotators[1].uid);
|
||||
});
|
||||
|
||||
it('should unbind onNotesLoaded on destruction', function() {
|
||||
annotators[0].destroy();
|
||||
expect($.fn.off).toHaveBeenCalledWith(
|
||||
'click',
|
||||
annotators[0].onNoteClick
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
54
lms/static/js/spec/edxnotes/views/tab_item_spec.js
Normal file
54
lms/static/js/spec/edxnotes/views/tab_item_spec.js
Normal file
@@ -0,0 +1,54 @@
|
||||
define([
|
||||
'jquery', 'js/common_helpers/template_helpers', 'js/edxnotes/collections/tabs',
|
||||
'js/edxnotes/views/tabs_list', 'js/spec/edxnotes/custom_matchers', 'jasmine-jquery'
|
||||
], function($, TemplateHelpers, TabsCollection, TabsListView, customMatchers) {
|
||||
'use strict';
|
||||
describe('EdxNotes TabItemView', function() {
|
||||
beforeEach(function () {
|
||||
customMatchers(this);
|
||||
TemplateHelpers.installTemplate('templates/edxnotes/tab-item');
|
||||
this.collection = new TabsCollection([
|
||||
{identifier: 'first-item'},
|
||||
{
|
||||
identifier: 'second-item',
|
||||
is_closable: true,
|
||||
icon: 'icon-class'
|
||||
}
|
||||
]);
|
||||
this.tabsList = new TabsListView({
|
||||
collection: this.collection
|
||||
}).render();
|
||||
});
|
||||
|
||||
it('can contain an icon', function () {
|
||||
var firstItem = this.tabsList.$('#first-item'),
|
||||
secondItem = this.tabsList.$('#second-item');
|
||||
|
||||
expect(firstItem.find('.icon')).not.toExist();
|
||||
expect(secondItem.find('.icon')).toHaveClass('icon-class');
|
||||
});
|
||||
|
||||
it('can navigate between tabs', function () {
|
||||
var firstItem = this.tabsList.$('#first-item'),
|
||||
secondItem = this.tabsList.$('#second-item');
|
||||
|
||||
expect(firstItem).toHaveClass('is-active'); // first tab is active
|
||||
expect(firstItem).toContainText('Current tab');
|
||||
expect(secondItem).not.toHaveClass('is-active'); // second tab is not active
|
||||
expect(secondItem).not.toContainText('Current tab');
|
||||
secondItem.click();
|
||||
expect(firstItem).not.toHaveClass('is-active'); // first tab is not active
|
||||
expect(firstItem).not.toContainText('Current tab');
|
||||
expect(secondItem).toHaveClass('is-active'); // second tab is active
|
||||
expect(secondItem).toContainText('Current tab');
|
||||
});
|
||||
|
||||
it('can close the tab', function () {
|
||||
var secondItem = this.tabsList.$('#second-item');
|
||||
|
||||
expect(this.tabsList.$('.tab')).toHaveLength(2);
|
||||
secondItem.find('.action-close').click();
|
||||
expect(this.tabsList.$('.tab')).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
117
lms/static/js/spec/edxnotes/views/tab_view_spec.js
Normal file
117
lms/static/js/spec/edxnotes/views/tab_view_spec.js
Normal file
@@ -0,0 +1,117 @@
|
||||
define([
|
||||
'jquery', 'backbone', 'js/common_helpers/template_helpers', 'js/edxnotes/collections/tabs',
|
||||
'js/edxnotes/views/tabs_list', 'js/edxnotes/views/tab_view',
|
||||
'js/spec/edxnotes/custom_matchers', 'jasmine-jquery'
|
||||
], function(
|
||||
$, Backbone, TemplateHelpers, TabsCollection, TabsListView, TabView, customMatchers
|
||||
) {
|
||||
'use strict';
|
||||
describe('EdxNotes TabView', function() {
|
||||
var TestSubView = Backbone.View.extend({
|
||||
id: 'test-subview-panel',
|
||||
className: 'tab-panel',
|
||||
content: '<p>test view content</p>',
|
||||
render: function () {
|
||||
this.$el.html(this.content);
|
||||
return this;
|
||||
}
|
||||
}),
|
||||
TestView = TabView.extend({
|
||||
PanelConstructor: TestSubView,
|
||||
tabInfo: {
|
||||
name: 'Test View Tab',
|
||||
is_closable: true
|
||||
}
|
||||
}), getView;
|
||||
|
||||
getView = function (tabsCollection, options) {
|
||||
var view;
|
||||
options = _.defaults(options || {}, {
|
||||
el: $('.wrapper-student-notes'),
|
||||
collection: [],
|
||||
tabsCollection: tabsCollection
|
||||
});
|
||||
view = new TestView(options);
|
||||
|
||||
if (tabsCollection.length) {
|
||||
tabsCollection.at(0).activate();
|
||||
}
|
||||
|
||||
return view;
|
||||
};
|
||||
|
||||
beforeEach(function () {
|
||||
customMatchers(this);
|
||||
loadFixtures('js/fixtures/edxnotes/edxnotes.html');
|
||||
TemplateHelpers.installTemplates([
|
||||
'templates/edxnotes/note-item', 'templates/edxnotes/tab-item'
|
||||
]);
|
||||
this.tabsCollection = new TabsCollection();
|
||||
this.tabsList = new TabsListView({collection: this.tabsCollection}).render();
|
||||
this.tabsList.$el.appendTo($('.tab-list'));
|
||||
});
|
||||
|
||||
it('can create a tab and content on initialization', function () {
|
||||
var view = getView(this.tabsCollection);
|
||||
expect(this.tabsCollection).toHaveLength(1);
|
||||
expect(view.$('.tab')).toExist();
|
||||
expect(view.$('.wrapper-tabs')).toContainHtml('<p>test view content</p>');
|
||||
});
|
||||
|
||||
it('cannot create a tab on initialization if flag is not set', function () {
|
||||
var view = getView(this.tabsCollection, {
|
||||
createTabOnInitialization: false
|
||||
});
|
||||
expect(this.tabsCollection).toHaveLength(0);
|
||||
expect(view.$('.tab')).not.toExist();
|
||||
expect(view.$('.wrapper-tabs')).not.toContainHtml('<p>test view content</p>');
|
||||
});
|
||||
|
||||
it('can remove the content if tab becomes inactive', function () {
|
||||
var view = getView(this.tabsCollection);
|
||||
this.tabsCollection.add({identifier: 'second-tab'});
|
||||
view.$('#second-tab').click();
|
||||
expect(view.$('.tab')).toHaveLength(2);
|
||||
expect(view.$('.wrapper-tabs')).not.toContainHtml('<p>test view content</p>');
|
||||
});
|
||||
|
||||
it('can remove the content if tab is closed', function () {
|
||||
var view = getView(this.tabsCollection);
|
||||
view.onClose = jasmine.createSpy();
|
||||
view.$('.tab .action-close').click();
|
||||
expect(view.$('.tab')).toHaveLength(0);
|
||||
expect(view.$('.wrapper-tabs')).not.toContainHtml('<p>test view content</p>');
|
||||
expect(view.tabModel).toBeNull();
|
||||
expect(view.onClose).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('can correctly update the content of active tab', function () {
|
||||
var view = getView(this.tabsCollection);
|
||||
TestSubView.prototype.content = '<p>New content</p>';
|
||||
view.render();
|
||||
expect(view.$('.wrapper-tabs')).toContainHtml('<p>New content</p>');
|
||||
expect(view.$('.wrapper-tabs')).not.toContainHtml('<p>test view content</p>');
|
||||
});
|
||||
|
||||
it('can show/hide error messages', function () {
|
||||
var view = getView(this.tabsCollection),
|
||||
errorHolder = view.$('.wrapper-msg');
|
||||
view.showErrorMessage('<p>error message is here</p>');
|
||||
expect(errorHolder).not.toHaveClass('is-hidden');
|
||||
expect(errorHolder.find('.copy')).toContainHtml('<p>error message is here</p>');
|
||||
|
||||
view.hideErrorMessage();
|
||||
expect(errorHolder).toHaveClass('is-hidden');
|
||||
expect(errorHolder.find('.copy')).toBeEmpty();
|
||||
});
|
||||
|
||||
it('should hide error messages before rendering', function () {
|
||||
var view = getView(this.tabsCollection),
|
||||
errorHolder = view.$('.wrapper-msg');
|
||||
view.showErrorMessage('<p>error message is here</p>');
|
||||
view.render();
|
||||
expect(errorHolder).toHaveClass('is-hidden');
|
||||
expect(errorHolder.find('.copy')).toBeEmpty();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,67 @@
|
||||
define([
|
||||
'jquery', 'underscore', 'js/common_helpers/template_helpers', 'js/spec/edxnotes/helpers',
|
||||
'js/edxnotes/collections/notes', 'js/edxnotes/collections/tabs',
|
||||
'js/edxnotes/views/tabs/course_structure', 'js/spec/edxnotes/custom_matchers',
|
||||
'jasmine-jquery'
|
||||
], function(
|
||||
$, _, TemplateHelpers, Helpers, NotesCollection, TabsCollection, CourseStructureView,
|
||||
customMatchers
|
||||
) {
|
||||
'use strict';
|
||||
describe('EdxNotes CourseStructureView', function() {
|
||||
var notes = Helpers.getDefaultNotes(),
|
||||
getView, getText;
|
||||
|
||||
getText = function (selector) {
|
||||
return $(selector).map(function () {
|
||||
return _.trim($(this).text());
|
||||
}).toArray();
|
||||
};
|
||||
|
||||
getView = function (collection, tabsCollection, options) {
|
||||
var view;
|
||||
|
||||
options = _.defaults(options || {}, {
|
||||
el: $('.wrapper-student-notes'),
|
||||
collection: collection,
|
||||
tabsCollection: tabsCollection,
|
||||
});
|
||||
|
||||
view = new CourseStructureView(options);
|
||||
tabsCollection.at(0).activate();
|
||||
|
||||
return view;
|
||||
};
|
||||
|
||||
beforeEach(function () {
|
||||
customMatchers(this);
|
||||
loadFixtures('js/fixtures/edxnotes/edxnotes.html');
|
||||
TemplateHelpers.installTemplates([
|
||||
'templates/edxnotes/note-item', 'templates/edxnotes/tab-item'
|
||||
]);
|
||||
|
||||
this.collection = new NotesCollection(notes);
|
||||
this.tabsCollection = new TabsCollection();
|
||||
});
|
||||
|
||||
it('displays a tab and content with proper data and order', function () {
|
||||
var view = getView(this.collection, this.tabsCollection),
|
||||
chapters = getText('.course-title'),
|
||||
sections = getText('.course-subtitle'),
|
||||
notes = getText('.note-excerpt-p');
|
||||
|
||||
expect(this.tabsCollection).toHaveLength(1);
|
||||
expect(this.tabsCollection.at(0).toJSON()).toEqual({
|
||||
name: 'Location in Course',
|
||||
identifier: 'view-course-structure',
|
||||
icon: 'fa fa-list-ul',
|
||||
is_active: true,
|
||||
is_closable: false
|
||||
});
|
||||
expect(view.$('#structure-panel')).toExist();
|
||||
expect(chapters).toEqual(['First Chapter', 'Second Chapter']);
|
||||
expect(sections).toEqual(['First Section', 'Second Section', 'Third Section']);
|
||||
expect(notes).toEqual(['Note 1', 'Note 2', 'Note 3', 'Note 4', 'Note 5']);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,76 @@
|
||||
define([
|
||||
'jquery', 'js/common_helpers/template_helpers', 'js/edxnotes/collections/notes',
|
||||
'js/edxnotes/collections/tabs', 'js/edxnotes/views/tabs/recent_activity',
|
||||
'js/spec/edxnotes/custom_matchers', 'jasmine-jquery'
|
||||
], function(
|
||||
$, TemplateHelpers, NotesCollection, TabsCollection, RecentActivityView, customMatchers
|
||||
) {
|
||||
'use strict';
|
||||
describe('EdxNotes RecentActivityView', function() {
|
||||
var notes = [
|
||||
{
|
||||
created: 'December 11, 2014 at 11:12AM',
|
||||
updated: 'December 11, 2014 at 11:12AM',
|
||||
text: 'Third added model',
|
||||
quote: 'Should be listed first'
|
||||
},
|
||||
{
|
||||
created: 'December 11, 2014 at 11:11AM',
|
||||
updated: 'December 11, 2014 at 11:11AM',
|
||||
text: 'Second added model',
|
||||
quote: 'Should be listed second'
|
||||
},
|
||||
{
|
||||
created: 'December 11, 2014 at 11:10AM',
|
||||
updated: 'December 11, 2014 at 11:10AM',
|
||||
text: 'First added model',
|
||||
quote: 'Should be listed third'
|
||||
}
|
||||
], getView;
|
||||
|
||||
getView = function (collection, tabsCollection, options) {
|
||||
var view;
|
||||
|
||||
options = _.defaults(options || {}, {
|
||||
el: $('.wrapper-student-notes'),
|
||||
collection: collection,
|
||||
tabsCollection: tabsCollection,
|
||||
});
|
||||
|
||||
view = new RecentActivityView(options);
|
||||
tabsCollection.at(0).activate();
|
||||
|
||||
return view;
|
||||
};
|
||||
|
||||
beforeEach(function () {
|
||||
customMatchers(this);
|
||||
loadFixtures('js/fixtures/edxnotes/edxnotes.html');
|
||||
TemplateHelpers.installTemplates([
|
||||
'templates/edxnotes/note-item', 'templates/edxnotes/tab-item'
|
||||
]);
|
||||
|
||||
this.collection = new NotesCollection(notes);
|
||||
this.tabsCollection = new TabsCollection();
|
||||
});
|
||||
|
||||
it('displays a tab and content with proper data and order', function () {
|
||||
var view = getView(this.collection, this.tabsCollection);
|
||||
|
||||
expect(this.tabsCollection).toHaveLength(1);
|
||||
expect(this.tabsCollection.at(0).toJSON()).toEqual({
|
||||
name: 'Recent Activity',
|
||||
identifier: 'view-recent-activity',
|
||||
icon: 'fa fa-clock-o',
|
||||
is_active: true,
|
||||
is_closable: false
|
||||
});
|
||||
expect(view.$('#recent-panel')).toExist();
|
||||
expect(view.$('.note')).toHaveLength(3);
|
||||
_.each(view.$('.note'), function(element, index) {
|
||||
expect($('.note-comments', element)).toContainText(notes[index].text);
|
||||
expect($('.note-excerpt', element)).toContainText(notes[index].quote);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
205
lms/static/js/spec/edxnotes/views/tabs/search_results_spec.js
Normal file
205
lms/static/js/spec/edxnotes/views/tabs/search_results_spec.js
Normal file
@@ -0,0 +1,205 @@
|
||||
define([
|
||||
'jquery', 'js/common_helpers/template_helpers', 'js/common_helpers/ajax_helpers',
|
||||
'logger', 'js/edxnotes/collections/tabs', 'js/edxnotes/views/tabs/search_results',
|
||||
'js/spec/edxnotes/custom_matchers', 'jasmine-jquery'
|
||||
], function(
|
||||
$, TemplateHelpers, AjaxHelpers, Logger, TabsCollection, SearchResultsView,
|
||||
customMatchers
|
||||
) {
|
||||
'use strict';
|
||||
describe('EdxNotes SearchResultsView', function() {
|
||||
var notes = [
|
||||
{
|
||||
created: 'December 11, 2014 at 11:12AM',
|
||||
updated: 'December 11, 2014 at 11:12AM',
|
||||
text: 'Third added model',
|
||||
quote: 'Should be listed first'
|
||||
},
|
||||
{
|
||||
created: 'December 11, 2014 at 11:11AM',
|
||||
updated: 'December 11, 2014 at 11:11AM',
|
||||
text: 'Second added model',
|
||||
quote: 'Should be listed second'
|
||||
},
|
||||
{
|
||||
created: 'December 11, 2014 at 11:10AM',
|
||||
updated: 'December 11, 2014 at 11:10AM',
|
||||
text: 'First added model',
|
||||
quote: 'Should be listed third'
|
||||
}
|
||||
],
|
||||
responseJson = {
|
||||
total: 3,
|
||||
rows: notes
|
||||
},
|
||||
getView, submitForm;
|
||||
|
||||
getView = function (tabsCollection, options) {
|
||||
options = _.defaults(options || {}, {
|
||||
el: $('.wrapper-student-notes'),
|
||||
tabsCollection: tabsCollection,
|
||||
user: 'test_user',
|
||||
courseId: 'course_id',
|
||||
createTabOnInitialization: false
|
||||
});
|
||||
return new SearchResultsView(options);
|
||||
};
|
||||
|
||||
submitForm = function (searchBox, text) {
|
||||
searchBox.$('.search-notes-input').val(text);
|
||||
searchBox.$('.search-notes-submit').click();
|
||||
};
|
||||
|
||||
beforeEach(function () {
|
||||
customMatchers(this);
|
||||
loadFixtures('js/fixtures/edxnotes/edxnotes.html');
|
||||
TemplateHelpers.installTemplates([
|
||||
'templates/edxnotes/note-item', 'templates/edxnotes/tab-item'
|
||||
]);
|
||||
|
||||
this.tabsCollection = new TabsCollection();
|
||||
});
|
||||
|
||||
it('does not create a tab and content on initialization', function () {
|
||||
var view = getView(this.tabsCollection);
|
||||
expect(this.tabsCollection).toHaveLength(0);
|
||||
expect(view.$('#search-results-panel')).not.toExist();
|
||||
});
|
||||
|
||||
it('displays a tab and content on search with proper data and order', function () {
|
||||
var view = getView(this.tabsCollection),
|
||||
requests = AjaxHelpers.requests(this);
|
||||
|
||||
submitForm(view.searchBox, 'second');
|
||||
AjaxHelpers.respondWithJson(requests, responseJson);
|
||||
|
||||
expect(this.tabsCollection).toHaveLength(1);
|
||||
expect(this.tabsCollection.at(0).toJSON()).toEqual({
|
||||
name: 'Search Results',
|
||||
identifier: 'view-search-results',
|
||||
icon: 'fa fa-search',
|
||||
is_active: true,
|
||||
is_closable: true
|
||||
});
|
||||
expect(view.$('#search-results-panel')).toExist();
|
||||
expect(view.$('#search-results-panel')).toBeFocused();
|
||||
expect(view.$('.note')).toHaveLength(3);
|
||||
view.searchResults.collection.each(function (model, index) {
|
||||
expect(model.get('text')).toBe(notes[index].text);
|
||||
});
|
||||
});
|
||||
|
||||
it('displays loading indicator when search is running', function () {
|
||||
var view = getView(this.tabsCollection),
|
||||
requests = AjaxHelpers.requests(this);
|
||||
|
||||
submitForm(view.searchBox, 'test query');
|
||||
expect(view.$('.ui-loading')).not.toHaveClass('is-hidden');
|
||||
expect(view.$('.ui-loading')).toBeFocused();
|
||||
expect(this.tabsCollection).toHaveLength(1);
|
||||
expect(view.searchResults).toBeNull();
|
||||
expect(view.$('.tab-panel')).not.toExist();
|
||||
AjaxHelpers.respondWithJson(requests, responseJson);
|
||||
expect(view.$('.ui-loading')).toHaveClass('is-hidden');
|
||||
});
|
||||
|
||||
it('displays no results message', function () {
|
||||
var view = getView(this.tabsCollection),
|
||||
requests = AjaxHelpers.requests(this);
|
||||
|
||||
submitForm(view.searchBox, 'some text');
|
||||
AjaxHelpers.respondWithJson(requests, {
|
||||
total: 0,
|
||||
rows: []
|
||||
});
|
||||
|
||||
expect(view.$('#search-results-panel')).not.toExist();
|
||||
expect(view.$('#no-results-panel')).toBeFocused();
|
||||
expect(view.$('#no-results-panel')).toExist();
|
||||
expect(view.$('#no-results-panel')).toContainText(
|
||||
'No results found for "some text".'
|
||||
);
|
||||
});
|
||||
|
||||
it('does not send an additional request on switching between tabs', function () {
|
||||
var view = getView(this.tabsCollection),
|
||||
requests = AjaxHelpers.requests(this);
|
||||
|
||||
spyOn(Logger, 'log');
|
||||
submitForm(view.searchBox, 'test_query');
|
||||
AjaxHelpers.respondWithJson(requests, responseJson);
|
||||
|
||||
expect(requests).toHaveLength(1);
|
||||
|
||||
this.tabsCollection.add({});
|
||||
this.tabsCollection.at(1).activate();
|
||||
expect(view.$('#search-results-panel')).not.toExist();
|
||||
this.tabsCollection.at(0).activate();
|
||||
|
||||
expect(requests).toHaveLength(1);
|
||||
expect(view.$('#search-results-panel')).toExist();
|
||||
expect(view.$('.note')).toHaveLength(3);
|
||||
});
|
||||
|
||||
it('can clear search results if tab is closed', function () {
|
||||
var view = getView(this.tabsCollection),
|
||||
requests = AjaxHelpers.requests(this);
|
||||
|
||||
submitForm(view.searchBox, 'test_query');
|
||||
AjaxHelpers.respondWithJson(requests, responseJson);
|
||||
expect(view.searchResults).toBeDefined();
|
||||
this.tabsCollection.at(0).destroy();
|
||||
expect(view.searchResults).toBeNull();
|
||||
});
|
||||
|
||||
it('can correctly show/hide error messages', function () {
|
||||
var view = getView(this.tabsCollection),
|
||||
requests = AjaxHelpers.requests(this);
|
||||
|
||||
submitForm(view.searchBox, 'test error');
|
||||
requests[0].respond(
|
||||
500, {'Content-Type': 'application/json'},
|
||||
JSON.stringify({
|
||||
error: 'test error message'
|
||||
})
|
||||
);
|
||||
|
||||
expect(view.$('.wrapper-msg')).not.toHaveClass('is-hidden');
|
||||
expect(view.$('.wrapper-msg .copy')).toContainText('test error message');
|
||||
expect(view.$('.ui-loading')).toHaveClass('is-hidden');
|
||||
|
||||
submitForm(view.searchBox, 'Second');
|
||||
AjaxHelpers.respondWithJson(requests, responseJson);
|
||||
|
||||
expect(view.$('.wrapper-msg')).toHaveClass('is-hidden');
|
||||
expect(view.$('.wrapper-msg .copy')).toBeEmpty();
|
||||
});
|
||||
|
||||
it('can correctly update search results', function () {
|
||||
var view = getView(this.tabsCollection),
|
||||
requests = AjaxHelpers.requests(this),
|
||||
newNotes = [{
|
||||
created: 'December 11, 2014 at 11:10AM',
|
||||
updated: 'December 11, 2014 at 11:10AM',
|
||||
text: 'New Note',
|
||||
quote: 'New Note'
|
||||
}];
|
||||
|
||||
submitForm(view.searchBox, 'test_query');
|
||||
AjaxHelpers.respondWithJson(requests, responseJson);
|
||||
|
||||
expect(view.$('.note')).toHaveLength(3);
|
||||
|
||||
submitForm(view.searchBox, 'new_test_query');
|
||||
AjaxHelpers.respondWithJson(requests, {
|
||||
total: 1,
|
||||
rows: newNotes
|
||||
});
|
||||
|
||||
expect(view.$('.note').length).toHaveLength(1);
|
||||
view.searchResults.collection.each(function (model, index) {
|
||||
expect(model.get('text')).toBe(newNotes[index].text);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
50
lms/static/js/spec/edxnotes/views/tabs_list_spec.js
Normal file
50
lms/static/js/spec/edxnotes/views/tabs_list_spec.js
Normal file
@@ -0,0 +1,50 @@
|
||||
define([
|
||||
'jquery', 'js/common_helpers/template_helpers', 'js/edxnotes/collections/tabs',
|
||||
'js/edxnotes/views/tabs_list', 'js/spec/edxnotes/custom_matchers', 'jasmine-jquery'
|
||||
], function($, TemplateHelpers, TabsCollection, TabsListView, customMatchers) {
|
||||
'use strict';
|
||||
describe('EdxNotes TabsListView', function() {
|
||||
beforeEach(function () {
|
||||
customMatchers(this);
|
||||
TemplateHelpers.installTemplate('templates/edxnotes/tab-item');
|
||||
this.collection = new TabsCollection([
|
||||
{identifier: 'first-item'},
|
||||
{identifier: 'second-item'}
|
||||
]);
|
||||
this.tabsList = new TabsListView({
|
||||
collection: this.collection
|
||||
}).render();
|
||||
});
|
||||
|
||||
it('has correct order and class names', function () {
|
||||
var firstItem = this.tabsList.$('#first-item'),
|
||||
secondItem = this.tabsList.$('#second-item');
|
||||
|
||||
expect(firstItem).toHaveIndex(0);
|
||||
expect(firstItem).toHaveClass('is-active');
|
||||
expect(secondItem).toHaveIndex(1);
|
||||
});
|
||||
|
||||
it('can add a new tab', function () {
|
||||
var firstItem = this.tabsList.$('#first-item'),
|
||||
thirdItem;
|
||||
|
||||
this.collection.add({identifier: 'third-item'});
|
||||
thirdItem = this.tabsList.$('#third-item');
|
||||
|
||||
expect(firstItem).toHaveClass('is-active'); // first tab is still active
|
||||
expect(thirdItem).toHaveIndex(2);
|
||||
expect(this.tabsList.$('.tab')).toHaveLength(3);
|
||||
});
|
||||
|
||||
it('can remove tabs', function () {
|
||||
var secondItem = this.tabsList.$('#second-item');
|
||||
|
||||
this.collection.at(0).destroy(); // remove first tab
|
||||
expect(this.tabsList.$('.tab')).toHaveLength(1);
|
||||
expect(secondItem).toHaveClass('is-active'); // second tab becomes active
|
||||
this.collection.at(0).destroy();
|
||||
expect(this.tabsList.$('.tab')).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,99 @@
|
||||
define([
|
||||
'jquery', 'annotator', 'js/common_helpers/ajax_helpers', 'js/edxnotes/views/visibility_decorator',
|
||||
'js/edxnotes/views/toggle_notes_factory', 'js/spec/edxnotes/helpers',
|
||||
'js/spec/edxnotes/custom_matchers', 'jasmine-jquery'
|
||||
], function(
|
||||
$, Annotator, AjaxHelpers, VisibilityDecorator, ToggleNotesFactory, Helpers,
|
||||
customMatchers
|
||||
) {
|
||||
'use strict';
|
||||
describe('EdxNotes ToggleNotesFactory', function() {
|
||||
var params = {
|
||||
endpoint: '/test_endpoint',
|
||||
user: 'a user',
|
||||
usageId : 'an usage',
|
||||
courseId: 'a course',
|
||||
token: Helpers.makeToken(),
|
||||
tokenUrl: '/test_token_url'
|
||||
};
|
||||
|
||||
beforeEach(function() {
|
||||
customMatchers(this);
|
||||
loadFixtures(
|
||||
'js/fixtures/edxnotes/edxnotes_wrapper.html',
|
||||
'js/fixtures/edxnotes/toggle_notes.html'
|
||||
);
|
||||
VisibilityDecorator.factory(
|
||||
document.getElementById('edx-notes-wrapper-123'), params, true
|
||||
);
|
||||
VisibilityDecorator.factory(
|
||||
document.getElementById('edx-notes-wrapper-456'), params, true
|
||||
);
|
||||
this.toggleNotes = ToggleNotesFactory(true, '/test_url');
|
||||
this.button = $('.action-toggle-notes');
|
||||
this.label = this.button.find('.utility-control-label');
|
||||
this.toggleMessage = $('.action-toggle-message');
|
||||
});
|
||||
|
||||
afterEach(function () {
|
||||
VisibilityDecorator._setVisibility(null);
|
||||
_.invoke(Annotator._instances, 'destroy');
|
||||
$('.annotator-notice').remove();
|
||||
});
|
||||
|
||||
it('can toggle notes', function() {
|
||||
var requests = AjaxHelpers.requests(this);
|
||||
|
||||
expect(this.button).not.toHaveClass('is-disabled');
|
||||
expect(this.label).toContainText('Hide notes');
|
||||
expect(this.button).toHaveClass('is-active');
|
||||
expect(this.button).toHaveAttr('aria-pressed', 'true');
|
||||
expect(this.toggleMessage).not.toHaveClass('is-fleeting');
|
||||
expect(this.toggleMessage).toContainText('Hiding notes');
|
||||
|
||||
this.button.click();
|
||||
expect(this.label).toContainText('Show notes');
|
||||
expect(this.button).not.toHaveClass('is-active');
|
||||
expect(this.button).toHaveAttr('aria-pressed', 'false');
|
||||
expect(this.toggleMessage).toHaveClass('is-fleeting');
|
||||
expect(this.toggleMessage).toContainText('Hiding notes');
|
||||
expect(Annotator._instances).toHaveLength(0);
|
||||
|
||||
AjaxHelpers.expectJsonRequest(requests, 'PUT', '/test_url', {
|
||||
'visibility': false
|
||||
});
|
||||
AjaxHelpers.respondWithJson(requests, {});
|
||||
|
||||
this.button.click();
|
||||
expect(this.label).toContainText('Hide notes');
|
||||
expect(this.button).toHaveClass('is-active');
|
||||
expect(this.button).toHaveAttr('aria-pressed', 'true');
|
||||
expect(this.toggleMessage).toHaveClass('is-fleeting');
|
||||
expect(this.toggleMessage).toContainText('Showing notes');
|
||||
expect(Annotator._instances).toHaveLength(2);
|
||||
|
||||
AjaxHelpers.expectJsonRequest(requests, 'PUT', '/test_url', {
|
||||
'visibility': true
|
||||
});
|
||||
AjaxHelpers.respondWithJson(requests, {});
|
||||
});
|
||||
|
||||
it('can handle errors', function() {
|
||||
var requests = AjaxHelpers.requests(this),
|
||||
errorContainer = $('.annotator-notice');
|
||||
|
||||
this.button.click();
|
||||
AjaxHelpers.respondWithError(requests);
|
||||
expect(errorContainer).toContainText(
|
||||
"An error has occurred. Make sure that you are connected to the Internet, and then try refreshing the page."
|
||||
);
|
||||
expect(errorContainer).toBeVisible();
|
||||
expect(errorContainer).toHaveClass('annotator-notice-show');
|
||||
expect(errorContainer).toHaveClass('annotator-notice-error');
|
||||
|
||||
this.button.click();
|
||||
AjaxHelpers.respondWithJson(requests, {});
|
||||
expect(errorContainer).not.toHaveClass('annotator-notice-show');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,56 @@
|
||||
define([
|
||||
'annotator', 'js/edxnotes/views/visibility_decorator',
|
||||
'js/spec/edxnotes/helpers', 'js/spec/edxnotes/custom_matchers'
|
||||
], function(Annotator, VisibilityDecorator, Helpers, customMatchers) {
|
||||
'use strict';
|
||||
describe('EdxNotes VisibilityDecorator', function() {
|
||||
var params = {
|
||||
endpoint: '/test_endpoint',
|
||||
user: 'a user',
|
||||
usageId : 'an usage',
|
||||
courseId: 'a course',
|
||||
token: Helpers.makeToken(),
|
||||
tokenUrl: '/test_token_url'
|
||||
};
|
||||
|
||||
beforeEach(function() {
|
||||
customMatchers(this);
|
||||
loadFixtures('js/fixtures/edxnotes/edxnotes_wrapper.html');
|
||||
this.wrapper = document.getElementById('edx-notes-wrapper-123');
|
||||
});
|
||||
|
||||
afterEach(function () {
|
||||
VisibilityDecorator._setVisibility(null);
|
||||
_.invoke(Annotator._instances, 'destroy');
|
||||
});
|
||||
|
||||
it('can initialize Notes if it visibility equals True', function() {
|
||||
var note = VisibilityDecorator.factory(this.wrapper, params, true);
|
||||
expect(note).toEqual(jasmine.any(Annotator));
|
||||
});
|
||||
|
||||
it('does not initialize Notes if it visibility equals False', function() {
|
||||
var note = VisibilityDecorator.factory(this.wrapper, params, false);
|
||||
expect(note).toBeNull();
|
||||
});
|
||||
|
||||
it('can disable all notes', function() {
|
||||
VisibilityDecorator.factory(this.wrapper, params, true);
|
||||
VisibilityDecorator.factory(document.getElementById('edx-notes-wrapper-456'), params, true);
|
||||
|
||||
VisibilityDecorator.disableNotes();
|
||||
expect(Annotator._instances).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('can enable the note', function() {
|
||||
var secondWrapper = document.getElementById('edx-notes-wrapper-456');
|
||||
VisibilityDecorator.factory(this.wrapper, params, false);
|
||||
VisibilityDecorator.factory(secondWrapper, params, false);
|
||||
|
||||
VisibilityDecorator.enableNote(this.wrapper);
|
||||
expect(Annotator._instances).toHaveLength(1);
|
||||
VisibilityDecorator.enableNote(secondWrapper);
|
||||
expect(Annotator._instances).toHaveLength(2);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,5 +1,4 @@
|
||||
(function(requirejs, define) {
|
||||
|
||||
// TODO: how can we share the vast majority of this config that is in common with CMS?
|
||||
requirejs.config({
|
||||
paths: {
|
||||
@@ -54,6 +53,7 @@
|
||||
'xblock/lms.runtime.v1': 'coffee/src/xblock/lms.runtime.v1',
|
||||
'capa/display': 'xmodule_js/src/capa/display',
|
||||
'string_utils': 'xmodule_js/common_static/js/src/string_utils',
|
||||
'logger': 'xmodule_js/common_static/js/src/logger',
|
||||
|
||||
// Manually specify LMS files that are not converted to RequireJS
|
||||
'history': 'js/vendor/history',
|
||||
@@ -77,7 +77,10 @@
|
||||
'js/student_account/models/RegisterModel': 'js/student_account/models/RegisterModel',
|
||||
'js/student_account/views/RegisterView': 'js/student_account/views/RegisterView',
|
||||
'js/student_account/views/AccessView': 'js/student_account/views/AccessView',
|
||||
'js/student_profile/profile': 'js/student_profile/profile'
|
||||
'js/student_profile/profile': 'js/student_profile/profile',
|
||||
|
||||
// edxnotes
|
||||
'annotator': 'xmodule_js/common_static/js/vendor/edxnotes/annotator-full.min'
|
||||
},
|
||||
shim: {
|
||||
'gettext': {
|
||||
@@ -211,6 +214,9 @@
|
||||
'xmodule': {
|
||||
exports: 'XModule'
|
||||
},
|
||||
'logger': {
|
||||
exports: 'Logger'
|
||||
},
|
||||
'sinon': {
|
||||
exports: 'sinon'
|
||||
},
|
||||
@@ -488,6 +494,11 @@
|
||||
'js/verify_student/views/enrollment_confirmation_step_view'
|
||||
]
|
||||
},
|
||||
// Student Notes
|
||||
'annotator': {
|
||||
exports: 'Annotator',
|
||||
deps: ['jquery']
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -514,7 +525,26 @@
|
||||
'lms/include/js/spec/verify_student/pay_and_verify_view_spec.js',
|
||||
'lms/include/js/spec/verify_student/webcam_photo_view_spec.js',
|
||||
'lms/include/js/spec/verify_student/review_photos_step_view_spec.js',
|
||||
'lms/include/js/spec/verify_student/make_payment_step_view_spec.js'
|
||||
'lms/include/js/spec/verify_student/make_payment_step_view_spec.js',
|
||||
'lms/include/js/spec/edxnotes/utils/logger_spec.js',
|
||||
'lms/include/js/spec/edxnotes/views/notes_factory_spec.js',
|
||||
'lms/include/js/spec/edxnotes/views/shim_spec.js',
|
||||
'lms/include/js/spec/edxnotes/views/note_item_spec.js',
|
||||
'lms/include/js/spec/edxnotes/views/notes_page_spec.js',
|
||||
'lms/include/js/spec/edxnotes/views/search_box_spec.js',
|
||||
'lms/include/js/spec/edxnotes/views/tabs_list_spec.js',
|
||||
'lms/include/js/spec/edxnotes/views/tab_item_spec.js',
|
||||
'lms/include/js/spec/edxnotes/views/tab_view_spec.js',
|
||||
'lms/include/js/spec/edxnotes/views/tabs/search_results_spec.js',
|
||||
'lms/include/js/spec/edxnotes/views/tabs/recent_activity_spec.js',
|
||||
'lms/include/js/spec/edxnotes/views/tabs/course_structure_spec.js',
|
||||
'lms/include/js/spec/edxnotes/views/visibility_decorator_spec.js',
|
||||
'lms/include/js/spec/edxnotes/views/toggle_notes_factory_spec.js',
|
||||
'lms/include/js/spec/edxnotes/models/tab_spec.js',
|
||||
'lms/include/js/spec/edxnotes/models/note_spec.js',
|
||||
'lms/include/js/spec/edxnotes/plugins/events_spec.js',
|
||||
'lms/include/js/spec/edxnotes/plugins/scroller_spec.js',
|
||||
'lms/include/js/spec/edxnotes/collections/notes_spec.js'
|
||||
]);
|
||||
|
||||
}).call(this, requirejs, define);
|
||||
|
||||
@@ -30,7 +30,7 @@ prepend_path: lms/static
|
||||
lib_paths:
|
||||
- xmodule_js/common_static/js/test/i18n.js
|
||||
- xmodule_js/common_static/coffee/src/ajax_prefix.js
|
||||
- xmodule_js/common_static/coffee/src/logger.js
|
||||
- xmodule_js/common_static/js/src/logger.js
|
||||
- xmodule_js/common_static/js/vendor/jasmine-jquery.js
|
||||
- xmodule_js/common_static/js/vendor/jasmine-imagediff.js
|
||||
- xmodule_js/common_static/js/vendor/require.js
|
||||
@@ -55,6 +55,9 @@ lib_paths:
|
||||
- xmodule_js/common_static/js/vendor/underscore-min.js
|
||||
- xmodule_js/common_static/js/vendor/underscore.string.min.js
|
||||
- xmodule_js/common_static/js/vendor/backbone-min.js
|
||||
- xmodule_js/common_static/js/vendor/edxnotes/annotator-full.min.js
|
||||
- xmodule_js/common_static/js/test/i18n.js
|
||||
- xmodule_js/common_static/js/vendor/date.js
|
||||
|
||||
# Paths to source JavaScript files
|
||||
src_paths:
|
||||
@@ -77,10 +80,12 @@ spec_paths:
|
||||
fixture_paths:
|
||||
- templates/instructor/instructor_dashboard_2
|
||||
- templates/dashboard
|
||||
- templates/edxnotes
|
||||
- templates/student_account
|
||||
- templates/student_profile
|
||||
- templates/verify_student
|
||||
- templates/file-upload.underscore
|
||||
- js/fixtures/edxnotes
|
||||
|
||||
requirejs:
|
||||
paths:
|
||||
|
||||
@@ -30,7 +30,7 @@ prepend_path: lms/static
|
||||
lib_paths:
|
||||
- xmodule_js/common_static/js/test/i18n.js
|
||||
- xmodule_js/common_static/coffee/src/ajax_prefix.js
|
||||
- xmodule_js/common_static/coffee/src/logger.js
|
||||
- xmodule_js/common_static/js/src/logger.js
|
||||
- xmodule_js/common_static/js/vendor/jasmine-jquery.js
|
||||
- xmodule_js/common_static/js/vendor/jasmine-imagediff.js
|
||||
- xmodule_js/common_static/js/vendor/require.js
|
||||
|
||||
@@ -1,8 +1,29 @@
|
||||
;(function (require, define, _) {
|
||||
;(function (require, define) {
|
||||
var paths = {}, config;
|
||||
|
||||
// URI, tinymce, or jquery.tinymce may already have been loaded before the OVA templates and we do not want to load
|
||||
// them a second time. Check if it is the case and use the global var in requireJS config.
|
||||
// jquery, underscore, gettext, URI, tinymce, or jquery.tinymce may already
|
||||
// have been loaded and we do not want to load them a second time. Check if
|
||||
// it is the case and use the global var instead.
|
||||
if (window.jQuery) {
|
||||
define("jquery", [], function() {return window.jQuery;});
|
||||
} else {
|
||||
paths.jquery = "js/vendor/jquery.min";
|
||||
}
|
||||
if (window._) {
|
||||
define("underscore", [], function() {return window._;});
|
||||
} else {
|
||||
paths.jquery = "js/vendor/underscore-min";
|
||||
}
|
||||
if (window.gettext) {
|
||||
define("gettext", [], function() {return window.gettext;});
|
||||
} else {
|
||||
paths.gettext = "/i18n";
|
||||
}
|
||||
if (window.Logger) {
|
||||
define("logger", [], function() {return window.Logger;});
|
||||
} else {
|
||||
paths.logger = "js/src/logger";
|
||||
}
|
||||
if (window.URI) {
|
||||
define("URI", [], function() {return window.URI;});
|
||||
} else {
|
||||
@@ -20,10 +41,14 @@
|
||||
}
|
||||
|
||||
config = {
|
||||
// NOTE: baseUrl has been previously set in lms/templates/main.html
|
||||
// NOTE: baseUrl has been previously set in lms/static/templates/main.html
|
||||
waitSeconds: 60,
|
||||
paths: {
|
||||
// Files only needed for OVA
|
||||
"annotator_1.2.9": "js/vendor/edxnotes/annotator-full.min",
|
||||
"date": "js/vendor/date",
|
||||
"backbone": "js/vendor/backbone-min",
|
||||
"underscore.string": "js/vendor/underscore.string.min",
|
||||
// Files needed by OVA
|
||||
"annotator": "js/vendor/ova/annotator-full",
|
||||
"annotator-harvardx": "js/vendor/ova/annotator-full-firebase-auth",
|
||||
"video.dev": "js/vendor/ova/video.dev",
|
||||
@@ -42,10 +67,30 @@
|
||||
"ova": 'js/vendor/ova/ova',
|
||||
"catch": 'js/vendor/ova/catch/js/catch',
|
||||
"handlebars": 'js/vendor/ova/catch/js/handlebars-1.1.2',
|
||||
// end of files only needed for OVA
|
||||
// end of files needed by OVA
|
||||
},
|
||||
shim: {
|
||||
// The following are all needed for OVA
|
||||
"annotator_1.2.9": {
|
||||
deps: ["jquery"],
|
||||
exports: "Annotator"
|
||||
},
|
||||
"date": {
|
||||
exports: "Date"
|
||||
},
|
||||
"jquery": {
|
||||
exports: "$"
|
||||
},
|
||||
"underscore": {
|
||||
exports: "_"
|
||||
},
|
||||
"backbone": {
|
||||
deps: ["underscore", "jquery"],
|
||||
exports: "Backbone"
|
||||
},
|
||||
"logger": {
|
||||
exports: "Logger"
|
||||
},
|
||||
// Needed by OVA
|
||||
"video.dev": {
|
||||
exports:"videojs"
|
||||
},
|
||||
@@ -74,7 +119,7 @@
|
||||
deps: ["annotator"]
|
||||
},
|
||||
"diacritic-annotator": {
|
||||
deps: ["annotator"]
|
||||
deps: ["annotator"]
|
||||
},
|
||||
"flagging-annotator": {
|
||||
deps: ["annotator"]
|
||||
@@ -99,9 +144,19 @@
|
||||
"URI"
|
||||
]
|
||||
},
|
||||
// End of OVA
|
||||
// End of needed by OVA
|
||||
},
|
||||
map: {
|
||||
"js/edxnotes/*": {
|
||||
"annotator": "annotator_1.2.9"
|
||||
}
|
||||
}
|
||||
};
|
||||
_.extend(config.paths, paths);
|
||||
|
||||
for (var key in paths) {
|
||||
if ({}.hasOwnProperty.call(paths, key)) {
|
||||
config.paths[key] = paths[key];
|
||||
}
|
||||
}
|
||||
require.config(config);
|
||||
}).call(this, require || RequireJS.require, define || RequireJS.define, _);
|
||||
}).call(this, require || RequireJS.require, define || RequireJS.define);
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -261,3 +261,21 @@
|
||||
@-webkit-keyframes fade-in-animation{ @include fade-in-keyframes; }
|
||||
@-moz-keyframes fade-in-animation{ @include fade-in-keyframes; }
|
||||
@keyframes fade-in-animation{ @include fade-in-keyframes; }
|
||||
|
||||
|
||||
// +utility animations
|
||||
// --------------------
|
||||
// pulse - double + fade out
|
||||
@include keyframes(pulse-out) {
|
||||
0%, 100% {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
25%, 75% {
|
||||
opacity: 1.0;
|
||||
}
|
||||
|
||||
100% {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -164,3 +164,42 @@
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
// border control
|
||||
%no-border-top {
|
||||
border-top: none;
|
||||
}
|
||||
|
||||
%no-border-bottom {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
%no-border-left {
|
||||
border-left: none;
|
||||
}
|
||||
|
||||
%no-border-right {
|
||||
border-right: none;
|
||||
}
|
||||
|
||||
// outline
|
||||
%no-outline {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
// shame-based mixins to centrally override poor styling
|
||||
%shame-link-base {
|
||||
color: $link-color;
|
||||
|
||||
&:hover, &:focus {
|
||||
color: saturate($link-color, 50%);
|
||||
}
|
||||
}
|
||||
|
||||
%shame-link-text {
|
||||
@extend %shame-link-base;
|
||||
|
||||
&:hover, &:focus {
|
||||
text-decoration: underline !important;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -329,6 +329,8 @@ $header-graphic-sub-color: $m-gray-d2;
|
||||
$error-color: $error-red;
|
||||
$warning-color: $m-pink;
|
||||
$confirm-color: $m-green;
|
||||
$active-color: $blue;
|
||||
$highlight-color: rgb(255,255,0);
|
||||
|
||||
// Notifications
|
||||
$notify-banner-bg-1: rgb(56,56,56);
|
||||
@@ -444,3 +446,13 @@ $blue1: #4A90E2;
|
||||
$blue2: #00A1E5;
|
||||
$green1: #61A12E;
|
||||
$red1: #D0021B;
|
||||
|
||||
// +case: search/result highlight
|
||||
// --------------------
|
||||
$result-highlight-color-base: rgba($highlight-color, 0.25);
|
||||
|
||||
// +feature: student notes
|
||||
// --------------------
|
||||
$student-notes-highlight-color-base: saturate($yellow, 65%);
|
||||
$student-notes-highlight-color: tint($student-notes-highlight-color-base, 50%);
|
||||
$student-notes-highlight-color-focus: $student-notes-highlight-color-base;
|
||||
|
||||
@@ -27,36 +27,37 @@
|
||||
// base - elements
|
||||
@import 'elements/typography';
|
||||
@import 'elements/controls';
|
||||
@import 'elements/navigation'; // all archetypes of navigation
|
||||
|
||||
// Course base / layout styles
|
||||
|
||||
// course - base
|
||||
@import 'course/layout/courseware_header';
|
||||
@import 'course/layout/footer';
|
||||
@import 'course/base/mixins';
|
||||
@import 'course/base/base';
|
||||
@import 'course/base/extends';
|
||||
@import 'xmodule/modules/css/module-styles.scss';
|
||||
|
||||
// courseware
|
||||
@import 'course/courseware/courseware';
|
||||
@import 'course/courseware/sidebar';
|
||||
@import 'course/courseware/amplifier';
|
||||
@import 'course/layout/calculator';
|
||||
@import 'course/layout/timer';
|
||||
@import 'course/layout/chat';
|
||||
|
||||
// course-specific courseware (all styles in these files should be gated by a
|
||||
// course-specific class). This should be replaced with a better way of
|
||||
// providing course-specific styling.
|
||||
// course - modules
|
||||
@import 'course/modules/student-notes'; // student notes
|
||||
@import 'course/modules/calculator'; // calculator utility
|
||||
@import 'course/modules/timer'; // timer
|
||||
@import 'course/modules/chat'; // chat utility
|
||||
|
||||
// course - specific courses
|
||||
@import "course/courseware/courses/_cs188.scss";
|
||||
|
||||
// wiki
|
||||
// course - wiki
|
||||
@import "course/wiki/basic-html";
|
||||
@import "course/wiki/sidebar";
|
||||
@import "course/wiki/create";
|
||||
@import "course/wiki/wiki";
|
||||
@import "course/wiki/table";
|
||||
|
||||
// pages
|
||||
// course - views
|
||||
@import "course/info";
|
||||
@import "course/syllabus"; // TODO arjun replace w/ custom tabs, see courseware/courses.py
|
||||
@import "course/textbook";
|
||||
@@ -66,12 +67,13 @@
|
||||
@import "course/staff_grading";
|
||||
@import "course/rubric";
|
||||
@import "course/open_ended_grading";
|
||||
@import "course/student-notes";
|
||||
|
||||
// instructor
|
||||
// course - instructor-only views
|
||||
@import "course/instructor/instructor";
|
||||
@import "course/instructor/instructor_2";
|
||||
@import "course/instructor/email";
|
||||
@import "xmodule/descriptors/css/module-styles.scss";
|
||||
|
||||
// discussion
|
||||
// course - discussion
|
||||
@import "course/discussion/form-wmd-toolbar";
|
||||
|
||||
@@ -27,36 +27,37 @@
|
||||
// base - elements
|
||||
@import 'elements/typography';
|
||||
@import 'elements/controls';
|
||||
@import 'elements/navigation'; // all archetypes of navigation
|
||||
|
||||
// Course base / layout styles
|
||||
// course - base
|
||||
@import 'course/layout/courseware_header';
|
||||
@import 'course/layout/footer';
|
||||
@import 'course/base/mixins';
|
||||
@import 'course/base/base';
|
||||
@import 'course/base/extends';
|
||||
@import 'xmodule/modules/css/module-styles.scss';
|
||||
|
||||
// courseware
|
||||
@import 'course/courseware/courseware';
|
||||
@import 'course/courseware/sidebar';
|
||||
@import 'course/courseware/amplifier';
|
||||
@import 'course/layout/calculator';
|
||||
@import 'course/layout/timer';
|
||||
@import 'course/layout/chat';
|
||||
|
||||
// course-specific courseware (all styles in these files should be gated by a
|
||||
// course-specific class). This should be replaced with a better way of
|
||||
// providing course-specific styling.
|
||||
// course - modules
|
||||
@import 'course/modules/student-notes'; // student notes
|
||||
@import 'course/modules/calculator'; // calculator utility
|
||||
@import 'course/modules/timer'; // timer
|
||||
@import 'course/modules/chat'; // chat utility
|
||||
|
||||
|
||||
// course - specific courses
|
||||
@import "course/courseware/courses/_cs188.scss";
|
||||
|
||||
// wiki
|
||||
// course - wiki
|
||||
@import "course/wiki/basic-html";
|
||||
@import "course/wiki/sidebar";
|
||||
@import "course/wiki/create";
|
||||
@import "course/wiki/wiki";
|
||||
@import "course/wiki/table";
|
||||
|
||||
// pages
|
||||
// course - views
|
||||
@import "course/info";
|
||||
@import "course/syllabus"; // TODO arjun replace w/ custom tabs, see courseware/courses.py
|
||||
@import "course/textbook";
|
||||
@@ -66,12 +67,13 @@
|
||||
@import "course/staff_grading";
|
||||
@import "course/rubric";
|
||||
@import "course/open_ended_grading";
|
||||
@import "course/student-notes";
|
||||
|
||||
// instructor
|
||||
// course - instructor-only views
|
||||
@import "course/instructor/instructor";
|
||||
@import "course/instructor/instructor_2";
|
||||
@import "course/instructor/email";
|
||||
@import "xmodule/descriptors/css/module-styles.scss";
|
||||
|
||||
// discussion
|
||||
// course - discussion
|
||||
@import "course/discussion/form-wmd-toolbar";
|
||||
|
||||
398
lms/static/sass/course/_student-notes.scss
Normal file
398
lms/static/sass/course/_student-notes.scss
Normal file
@@ -0,0 +1,398 @@
|
||||
// LMS -- views -- student notes
|
||||
// ====================
|
||||
|
||||
// in this document:
|
||||
// --------------------
|
||||
// +notes
|
||||
// +base
|
||||
// ++header +and search
|
||||
// +local variables/utilities
|
||||
// +individual group of notes
|
||||
// +tabbed views
|
||||
// +search - no results
|
||||
// +search - error
|
||||
// +case - no notes made
|
||||
|
||||
// +notes:
|
||||
// --------------------
|
||||
// * this Sass partial contains all of the styling needed for the student notes listing view.
|
||||
// * for other notes styling referenced here, see the Sass partial contains the in-line student notes UI.
|
||||
|
||||
// +local variables/utilities:
|
||||
// --------------------
|
||||
$divider-visual-primary: ($baseline/5) solid $gray-l4;
|
||||
$divider-visual-secondary: ($baseline/10) solid $gray-l4;
|
||||
$divider-visual-tertiary: ($baseline/20) solid $gray-l4;
|
||||
|
||||
%notes-tab-control {
|
||||
@include transition(none);
|
||||
@extend %shame-link-base;
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
border-bottom: ($baseline/5) solid $transparent;
|
||||
}
|
||||
|
||||
.view-student-notes {
|
||||
|
||||
// +base:
|
||||
// --------------------
|
||||
.wrapper-student-notes {
|
||||
@include clearfix();
|
||||
padding-bottom: $baseline;
|
||||
|
||||
.student-notes {
|
||||
@include clearfix();
|
||||
@extend .content; // needed extend carried over from course handouts UI, but should be cleaned up
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
// +header +and search:
|
||||
// --------------------
|
||||
.title-search-container {
|
||||
@include clearfix();
|
||||
margin-bottom: $baseline;
|
||||
|
||||
.wrapper-title {
|
||||
@include float(left);
|
||||
width: flex-grid(7,12);
|
||||
|
||||
.page-title {
|
||||
@extend %t-title4;
|
||||
@extend %t-weight1;
|
||||
margin-bottom: 0;
|
||||
|
||||
.page-subtitle {
|
||||
@extend %t-title7;
|
||||
@extend %t-weight2;
|
||||
display: block;
|
||||
margin-top: ($baseline/4);
|
||||
color: $gray-l1;
|
||||
letter-spacing: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.wrapper-notes-search {
|
||||
@include float(right);
|
||||
width: flex-grid(5,12);
|
||||
@include text-align(right);
|
||||
}
|
||||
|
||||
.search-notes-input, .search-notes-submit {
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.search-notes-input {
|
||||
@extend %t-demi-strong;
|
||||
position: relative;
|
||||
@include right(-6px); // manually positioning input right next to submit
|
||||
width: 55%;
|
||||
padding: ($baseline/2) ($baseline*0.75);
|
||||
color: $gray-d3;
|
||||
}
|
||||
|
||||
.search-notes-submit {
|
||||
@extend %btn-inherited-primary;
|
||||
@extend %t-action2;
|
||||
padding: 8px $baseline 9px $baseline; // manually syncing up height with search input
|
||||
}
|
||||
}
|
||||
|
||||
// +individual group of notes
|
||||
// --------------------
|
||||
.note-group {
|
||||
border-top: $divider-visual-primary;
|
||||
margin: 0;
|
||||
padding-top: ($baseline*1.5);
|
||||
|
||||
// course structure labels
|
||||
.course-title {
|
||||
@extend %t-title6;
|
||||
@extend %t-weight4;
|
||||
margin: 0 0 ($baseline/2) 0;
|
||||
color: $gray-d3;
|
||||
}
|
||||
|
||||
.course-subtitle {
|
||||
@extend %t-title7;
|
||||
@extend %t-weight4;
|
||||
margin: 0 0 ($baseline/4) 0;
|
||||
border-bottom: $divider-visual-tertiary;
|
||||
padding-bottom: ($baseline/2);
|
||||
color: $gray-d3;
|
||||
}
|
||||
|
||||
// individual note
|
||||
.note {
|
||||
@include clearfix();
|
||||
margin: ($baseline*1.5) 0;
|
||||
|
||||
.wrapper-note-excerpts {
|
||||
@include transition(box-shadow $tmg-avg ease-in-out 0, border-color $tmg-avg ease-in-out 0);
|
||||
display: inline-block;
|
||||
width: flex-grid(9, 12);
|
||||
border: 1px solid $gray-l5;
|
||||
border-radius: ($baseline/10);
|
||||
|
||||
// note - highlighted content
|
||||
.note-excerpt {
|
||||
@include transition(background-color $tmg-avg ease-in-out 0);
|
||||
padding: $baseline;
|
||||
background: $student-notes-highlight-color;
|
||||
|
||||
.note-excerpt-p,
|
||||
.note-excerpt-ul,
|
||||
.note-excerpt-ol {
|
||||
@extend %t-copy-base;
|
||||
}
|
||||
}
|
||||
|
||||
.note-excerpt-more-link {
|
||||
@extend %t-copy-sub1;
|
||||
@extend %t-weight2;
|
||||
@extend %shame-link-text;
|
||||
display: inline;
|
||||
@include margin-left($baseline/4);
|
||||
}
|
||||
|
||||
// note - comment made on highlighted content
|
||||
.note-comments {
|
||||
@extend %ui-no-list;
|
||||
border-top: ($baseline/5) solid $student-notes-highlight-color-focus;
|
||||
|
||||
.note-comment {
|
||||
@include transition(color $tmg-avg ease-in-out 0);
|
||||
padding: ($baseline*0.75) $baseline;
|
||||
color: $gray;
|
||||
|
||||
.note-comment-title {
|
||||
@extend %t-title8;
|
||||
letter-spacing: ($baseline/20);
|
||||
margin: 0 0 ($baseline/4) 0;
|
||||
color: $gray-l2;
|
||||
}
|
||||
|
||||
.note-comment-p,
|
||||
.note-comment-ul,
|
||||
.note-comment-ol {
|
||||
@extend %t-copy-sub1;
|
||||
@extend %t-weight2;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.note-comment-ul,
|
||||
.note-comment-ol {
|
||||
padding: auto;
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
// CASE: when a comment has a term that matches a notes search query
|
||||
.note-highlight {
|
||||
background-color: $result-highlight-color-base;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// note reference
|
||||
.reference {
|
||||
@extend %t-copy-sub1;
|
||||
display: inline-block;
|
||||
width: flex-grid(3, 12);
|
||||
vertical-align: top;
|
||||
|
||||
.wrapper-reference-content {
|
||||
padding: 0 $baseline;
|
||||
color: $gray-l2;
|
||||
|
||||
.reference-title {
|
||||
@extend %t-title8;
|
||||
@extend %t-weight3;
|
||||
margin-top: $baseline;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: ($baseline/20);
|
||||
color: $gray-l2;
|
||||
|
||||
// CASE: first reference title of a note
|
||||
&:first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.reference-meta {
|
||||
@extend %t-weight2;
|
||||
color: $m-gray-d2;
|
||||
}
|
||||
|
||||
// needed for poor base LMS styling scope
|
||||
a.reference-meta {
|
||||
@extend %shame-link-text;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// STATE: hover/focus
|
||||
&:hover, &:focus {
|
||||
|
||||
.wrapper-note-excerpts {
|
||||
box-shadow: 0 2px 0 1px $shadow-l2;
|
||||
border-color: $gray-l4;
|
||||
}
|
||||
|
||||
.note-excerpt {
|
||||
background: $student-notes-highlight-color-focus;
|
||||
}
|
||||
|
||||
.note-comment {
|
||||
color: $gray-d2;
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// +tabbed views
|
||||
// --------------------
|
||||
.wrapper-tabs {
|
||||
|
||||
.tab-panel, .inline-error, .ui-loading {
|
||||
@extend %no-outline;
|
||||
}
|
||||
|
||||
.tab-panel.note-group {
|
||||
padding-top: 0;
|
||||
}
|
||||
|
||||
.inline-error {
|
||||
margin: ($baseline/2) 0;
|
||||
border-bottom: 1px solid $red;
|
||||
padding: 0 0 ($baseline/2) 0;
|
||||
color: $red;
|
||||
}
|
||||
|
||||
.tab-list {
|
||||
@include clearfix();
|
||||
position: relative;
|
||||
top: ($baseline/5);
|
||||
|
||||
.tabs-label, .tabs {
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.tabs-label {
|
||||
@extend %hd-lv5;
|
||||
margin-bottom: 0;
|
||||
padding: ($baseline*0.75) 0;
|
||||
@include padding-right($baseline);
|
||||
color: $gray-l2;
|
||||
font-weight: $font-semibold !important; // needed for poor base LMS styling scope
|
||||
}
|
||||
|
||||
.tabs {
|
||||
@include clearfix();
|
||||
@extend %ui-no-list;
|
||||
position: relative;
|
||||
bottom: -($baseline/4);
|
||||
}
|
||||
|
||||
.tab {
|
||||
position: relative;
|
||||
display: inline;
|
||||
|
||||
.tab-label {
|
||||
@extend %notes-tab-control;
|
||||
padding: ($baseline/2) ($baseline*0.75);
|
||||
text-align: center;
|
||||
|
||||
.icon {
|
||||
@include margin-right($baseline/10);
|
||||
}
|
||||
}
|
||||
|
||||
// STATE: active/current tab being viewed
|
||||
&.is-active {
|
||||
|
||||
.tab-label {
|
||||
border-bottom-color: $gray-d3;
|
||||
color: $gray-d3;
|
||||
}
|
||||
|
||||
// CASE: tab-label can be closed
|
||||
.action-close {
|
||||
border-bottom: ($baseline/5) solid $gray-d3;
|
||||
}
|
||||
}
|
||||
|
||||
// CASE: tab-label can be closed
|
||||
.action-close {
|
||||
@extend %notes-tab-control;
|
||||
position: relative;
|
||||
@include left(-($baseline*0.75));
|
||||
padding: ($baseline/2);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// +search - no results
|
||||
// --------------------
|
||||
// NOTE: not a lot of elements/classes to reference in this DOM
|
||||
#no-results-panel {
|
||||
p {
|
||||
@extend %t-copy-lead1;
|
||||
margin: ($baseline*1.5) 0;
|
||||
}
|
||||
}
|
||||
|
||||
// +search - error
|
||||
// --------------------
|
||||
.wrapper-msg {
|
||||
margin-bottom: $baseline;
|
||||
}
|
||||
|
||||
// +case - no notes made
|
||||
// --------------------
|
||||
.placeholder {
|
||||
background: $gray-l5;
|
||||
border-top: ($baseline/4) solid $active-color;
|
||||
padding: ($baseline*1.5);
|
||||
}
|
||||
|
||||
.placeholder-title {
|
||||
@extend %hd-lv3;
|
||||
margin-bottom: $baseline;
|
||||
text-transform: none; // reset needed for poor h2 element styling
|
||||
letter-spacing: 0; // reset needed for poor h2 element styling
|
||||
}
|
||||
|
||||
.placeholder-copy {
|
||||
@extend %t-copy-sub1;
|
||||
|
||||
ul {
|
||||
@extend %ui-no-list;
|
||||
|
||||
li {
|
||||
@extend %wipe-last-child;
|
||||
display: block;
|
||||
margin-bottom: ($baseline/2);
|
||||
}
|
||||
}
|
||||
|
||||
p, ul {
|
||||
margin-bottom: $baseline;
|
||||
}
|
||||
}
|
||||
|
||||
.placeholder-cta-copy {
|
||||
@extend %t-strong;
|
||||
|
||||
a {
|
||||
@extend %t-strong;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,6 @@
|
||||
// LMS -- modules -- calculator
|
||||
// ====================
|
||||
|
||||
div.calc-main {
|
||||
bottom: -126px;
|
||||
left: 0;
|
||||
@@ -5,39 +8,37 @@ div.calc-main {
|
||||
@include transition(bottom $tmg-avg linear 0s);
|
||||
-webkit-appearance: none;
|
||||
width: 100%;
|
||||
z-index: 99;
|
||||
|
||||
&.open {
|
||||
bottom: -36px;
|
||||
}
|
||||
|
||||
a.calc {
|
||||
@include text-hide();
|
||||
background: url("../images/calc-icon.png") rgba(#111, .9) no-repeat center;
|
||||
.calc {
|
||||
@include transition(background-color $tmg-f2 ease-in-out 0s);
|
||||
background: url("../images/calc-icon.png") $black-t1 no-repeat center;
|
||||
border-bottom: 0;
|
||||
border-radius: 3px 3px 0 0;
|
||||
color: $white;
|
||||
float: right;
|
||||
height: 20px;
|
||||
display: inline-block;
|
||||
margin-right: ($baseline/2);
|
||||
padding: 8px 12px;
|
||||
height: $baseline;
|
||||
margin-right: ($baseline*0.75);
|
||||
padding: $baseline;
|
||||
position: relative;
|
||||
top: -45px;
|
||||
width: 16px;
|
||||
top: -42px;
|
||||
width: ($baseline*0.75);
|
||||
|
||||
&:hover, &:focus {
|
||||
opacity: 0.8;
|
||||
background-color: $gray-d1;
|
||||
}
|
||||
|
||||
&.closed {
|
||||
background-image: url("../images/close-calc-icon.png");
|
||||
background-color: $black;
|
||||
top: -36px;
|
||||
}
|
||||
}
|
||||
|
||||
div#calculator_wrapper {
|
||||
background: rgba(#111, .9);
|
||||
background: $black;
|
||||
clear: both;
|
||||
max-height: 90px;
|
||||
position: relative;
|
||||
@@ -1,5 +1,5 @@
|
||||
/* Chat
|
||||
-------------------------------------------------- */
|
||||
// LMS -- modules -- chat
|
||||
// ====================
|
||||
#chat-wrapper {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
290
lms/static/sass/course/modules/_student-notes.scss
Normal file
290
lms/static/sass/course/modules/_student-notes.scss
Normal file
File diff suppressed because one or more lines are too long
@@ -1,3 +1,6 @@
|
||||
// LMS -- modules -- student notes
|
||||
// ====================
|
||||
|
||||
div.timer-main {
|
||||
@extend %ui-depth2;
|
||||
position: fixed;
|
||||
117
lms/static/sass/elements/_navigation.scss
Normal file
117
lms/static/sass/elements/_navigation.scss
Normal file
@@ -0,0 +1,117 @@
|
||||
// LMS -- elements -- navigation
|
||||
// ====================
|
||||
|
||||
// in this document:
|
||||
// --------------------
|
||||
// +notes
|
||||
// +skip navigation
|
||||
// +utility navigation
|
||||
// +toggling utilities
|
||||
// +case - calculator spacing
|
||||
|
||||
// +notes:
|
||||
// --------------------
|
||||
// this Sass partial should have its contents eventually abstracted out so that onboarding/non-coureware navigation is separate from in course-based navigation systems
|
||||
|
||||
|
||||
// +skip navigation
|
||||
// --------------------
|
||||
%nav-skip {
|
||||
@extend %text-sr;
|
||||
}
|
||||
|
||||
.nav-contents, .nav-skip {
|
||||
@extend %nav-skip;
|
||||
}
|
||||
|
||||
// +utility navigation (course utiltiies)
|
||||
// --------------------
|
||||
.nav-utilities {
|
||||
@extend %ui-depth3;
|
||||
position: fixed;
|
||||
right: ($baseline/4);
|
||||
bottom: 0;
|
||||
|
||||
.wrapper-utility {
|
||||
@extend %wipe-last-child;
|
||||
display: inline-block;
|
||||
vertical-align: bottom;
|
||||
@include margin-right(6px);
|
||||
}
|
||||
|
||||
.utility-control {
|
||||
@include transition(background-color $tmg-f2 ease-in-out 0s, color $tmg-f2 ease-in-out 0s);
|
||||
position: relative;
|
||||
bottom: -($baseline/5);
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
padding: ($baseline/2) ($baseline*0.75) ($baseline*0.75) ($baseline*0.75);
|
||||
background: $black-t1;
|
||||
color: $white;
|
||||
|
||||
// STATE: hover/active
|
||||
&:hover, &:active {
|
||||
background: $gray-d1;
|
||||
}
|
||||
|
||||
// STATE: is active/in use
|
||||
&.is-active {
|
||||
background: $gray-d1;
|
||||
}
|
||||
}
|
||||
|
||||
// specific reset styling for any controls that are button elements
|
||||
.utility-control-button {
|
||||
border: none;
|
||||
box-shadow: none;
|
||||
text-shadow: none;
|
||||
font-size: inherit;
|
||||
font-weight: inherit;
|
||||
line-height: 0;
|
||||
border-radius: 0;
|
||||
|
||||
// STATE: hover/active
|
||||
&:hover, &:active, &:focus {
|
||||
border: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
}
|
||||
|
||||
// specific utility navigation - student notes toggling
|
||||
.action-toggle-notes {
|
||||
@extend %no-outline;
|
||||
|
||||
// STATE: is active/in use
|
||||
&.is-active {
|
||||
color: $student-notes-highlight-color-base;
|
||||
}
|
||||
}
|
||||
|
||||
// +toggling utilities
|
||||
// --------------------
|
||||
.action-toggle-message {
|
||||
@extend %t-title8;
|
||||
@extend %t-strong;
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
@include right($baseline*2.5);
|
||||
display: inline-block;
|
||||
min-width: ($baseline*5);
|
||||
padding: ($baseline/2) ($baseline*0.75);
|
||||
opacity: 0;
|
||||
background-color: $gray-d1;
|
||||
color: $white;
|
||||
text-align: center;
|
||||
|
||||
// STATE: is fleeting/temporary
|
||||
&.is-fleeting {
|
||||
@include animation(pulse-out $tmg-s2 ease-in-out);
|
||||
}
|
||||
}
|
||||
|
||||
// +case - calculator spacing (needed for overriding calculator positioning)
|
||||
// --------------------
|
||||
&.has-utility-calculator {
|
||||
@include right($baseline*2.50);
|
||||
}
|
||||
}
|
||||
@@ -120,6 +120,10 @@
|
||||
border-top: 3px solid $alert-color;
|
||||
}
|
||||
|
||||
&.error {
|
||||
border-top: 3px solid $error-color;
|
||||
}
|
||||
|
||||
&.warning {
|
||||
border-top: 3px solid $warning-color;
|
||||
}
|
||||
|
||||
@@ -196,11 +196,11 @@
|
||||
|
||||
// typography weights
|
||||
%t-weight1 {
|
||||
font-weight: 300;
|
||||
font-weight: $font-light;
|
||||
}
|
||||
|
||||
%t-weight2 {
|
||||
font-weight: 400;
|
||||
font-weight: $font-regular;
|
||||
}
|
||||
|
||||
%t-weight3 {
|
||||
@@ -208,11 +208,11 @@
|
||||
}
|
||||
|
||||
%t-weight4 {
|
||||
font-weight: 600;
|
||||
font-weight: $font-semibold;
|
||||
}
|
||||
|
||||
%t-weight5 {
|
||||
font-weight: 700;
|
||||
font-weight: $font-bold;
|
||||
}
|
||||
|
||||
// ====================
|
||||
|
||||
Reference in New Issue
Block a user