diff --git a/cms/static/coffee/spec/main.coffee b/cms/static/coffee/spec/main.coffee index 6a50b56145..7c5a6f93f3 100644 --- a/cms/static/coffee/spec/main.coffee +++ b/cms/static/coffee/spec/main.coffee @@ -199,6 +199,8 @@ define([ "js/spec/utils/module_spec", "js/spec/models/explicit_url_spec" + "js/spec/views/baseview_spec", + "js/spec/utils/handle_iframe_binding_spec", # these tests are run separate in the cms-squire suite, due to process # isolation issues with Squire.js diff --git a/cms/static/js/base.js b/cms/static/js/base.js index b2b3d97123..bc33afc44f 100644 --- a/cms/static/js/base.js +++ b/cms/static/js/base.js @@ -1,6 +1,6 @@ require(["domReady", "jquery", "underscore", "gettext", "js/views/feedback_notification", "js/views/feedback_prompt", - "js/utils/get_date", "js/utils/module", "jquery.ui", "jquery.leanModal", "jquery.form", "jquery.smoothScroll"], - function(domReady, $, _, gettext, NotificationView, PromptView, DateUtils, ModuleUtils) { + "js/utils/get_date", "js/utils/module", "js/utils/handle_iframe_binding", "jquery.ui", "jquery.leanModal", "jquery.form", "jquery.smoothScroll"], + function(domReady, $, _, gettext, NotificationView, PromptView, DateUtils, ModuleUtils, IframeUtils) { var $body; var $newComponentItem; @@ -113,6 +113,8 @@ domReady(function() { $('.edit-subsection-publish-settings').on('change', '.start-date, .start-time', function() { $('.edit-subsection-publish-settings').find('.save-button').show(); }); + + IframeUtils.iframeBinding(); }); function smoothScrollLink(e) { diff --git a/cms/static/js/spec/utils/handle_iframe_binding_spec.js b/cms/static/js/spec/utils/handle_iframe_binding_spec.js new file mode 100644 index 0000000000..b41794e64e --- /dev/null +++ b/cms/static/js/spec/utils/handle_iframe_binding_spec.js @@ -0,0 +1,37 @@ +define( + [ + "jquery", "underscore", + "js/utils/handle_iframe_binding", + ], +function ($, _, IframeBinding) { + + describe("IframeBinding", function () { + var doc = document.implementation.createHTMLDocument("New Document"); + var iframe_html = ''; + iframe_html += ''; + iframe_html += ''; + doc.body.innerHTML = iframe_html; + + it("modifies src url of DOM iframe and embed elements when iframeBinding function is executed", function () { + expect($(doc).find("iframe")[0].src).toEqual("http://www.youtube.com/embed/NHd27UvY-lw"); + expect($(doc).find("iframe")[1].src).toEqual("http://www.youtube.com/embed/NHd27UvY-lw?allowFullScreen=false"); + expect($(doc).find("embed")[0].hasAttribute("wmode")).toBe(false); + + IframeBinding.iframeBinding(doc); + + //after calling iframeBinding function: src url of iframes should have "wmode=transparent" in its querystring + //and embed objects should have "wmode='transparent'" as an attribute + expect($(doc).find("iframe")[0].src).toEqual("http://www.youtube.com/embed/NHd27UvY-lw?wmode=transparent"); + expect($(doc).find("iframe")[1].src).toEqual("http://www.youtube.com/embed/NHd27UvY-lw?wmode=transparent&allowFullScreen=false"); + expect($(doc).find("embed")[0].hasAttribute("wmode")).toBe(true); + + iframe_html = IframeBinding.iframeBindingHtml(iframe_html); + + //after calling iframeBinding function: src url of iframes should have "wmode=transparent" in its querystring + //and embed objects should have "wmode='transparent'" as an attribute + expect(iframe_html).toEqual('' + + '' + + ''); + }); + }); +}); diff --git a/cms/static/js/spec/views/baseview_spec.js b/cms/static/js/spec/views/baseview_spec.js new file mode 100644 index 0000000000..d3cbbcebc1 --- /dev/null +++ b/cms/static/js/spec/views/baseview_spec.js @@ -0,0 +1,50 @@ +define( + [ + "jquery", "underscore", + "js/views/baseview", + "js/utils/handle_iframe_binding", + "sinon" + ], +function ($, _, BaseView, IframeBinding, sinon) { + + describe("BaseView check", function () { + var baseView; + var iframeBinding_spy; + + beforeEach(function () { + iframeBinding_spy = sinon.spy(IframeBinding, "iframeBinding"); + baseView = BaseView.prototype; + + spyOn(baseView, 'initialize'); + spyOn(baseView, 'beforeRender'); + spyOn(baseView, 'render'); + spyOn(baseView, 'afterRender').andCallThrough(); + }); + + afterEach(function () { + iframeBinding_spy.restore(); + }); + + it('calls before and after render functions when render of baseview is called', function () { + var baseview_temp = new BaseView() + baseview_temp.render(); + + expect(baseView.initialize).toHaveBeenCalled(); + expect(baseView.beforeRender).toHaveBeenCalled(); + expect(baseView.render).toHaveBeenCalled(); + expect(baseView.afterRender).toHaveBeenCalled(); + }); + + it('calls iframeBinding function when afterRender of baseview is called', function () { + var baseview_temp = new BaseView() + baseview_temp.render(); + expect(baseView.afterRender).toHaveBeenCalled(); + expect(iframeBinding_spy.called).toEqual(true); + + //check calls count of iframeBinding function + expect(iframeBinding_spy.callCount).toBe(1); + IframeBinding.iframeBinding(); + expect(iframeBinding_spy.callCount).toBe(2); + }); + }); +}); diff --git a/cms/static/js/utils/handle_iframe_binding.js b/cms/static/js/utils/handle_iframe_binding.js new file mode 100644 index 0000000000..4a9f5dc798 --- /dev/null +++ b/cms/static/js/utils/handle_iframe_binding.js @@ -0,0 +1,61 @@ +define(["jquery"], function($) { + var iframeBinding = function (e) { + var target_element = null; + if (typeof(e) == "undefined"){ + target_element = $("iframe, embed"); + } else { + if (typeof(e.nodeName) != 'undefined'){ + target_element = $(e).find("iframe, embed"); + } else{ + target_element = e.$("iframe, embed"); + } + } + modifyTagContent(target_element); + }; + + var modifyTagContent = function (target_element) { + target_element.each(function(){ + if ($(this).prop('tagName') == 'IFRAME'){ + var ifr_source = $(this).attr('src'); + var wmode = "wmode=transparent"; + if(ifr_source.indexOf('?') != -1) { + var getQString = ifr_source.split('?'); + if (getQString[1].search('wmode=transparent') == -1){ + var oldString = getQString[1]; + var newString = getQString[0]; + $(this).attr('src',newString+'?'+wmode+'&'+oldString); + } + } + else $(this).attr('src',ifr_source+'?'+wmode); + } + else{ + $(this).attr('wmode', 'transparent'); + } + }); + }; + + // Modify iframe/embed tags in provided html string + // Use this method when provided data is just html sting not dom element + // This method will only modify iframe (add wmode=transparent in url querystring) and embed (add wmode=transparent as attribute) + // tags in html string so both tags will attach to dom and don't create z-index problem for other popups + // Note: embed tags should be modified before rendering as they are static objects as compared to iframes + // Note: this method can modify unintended html (invalid tags) while converting to dom object + var iframeBindingHtml = function (html_string) { + if (html_string){ + var target_element = null; + var temp_content = document.createElement('div'); + $(temp_content).html(html_string); + target_element = $(temp_content).find("iframe, embed"); + if (target_element.length > 0){ + modifyTagContent(target_element); + html_string = $(temp_content).html(); + } + } + return html_string; + }; + + return { + iframeBinding: iframeBinding, + iframeBindingHtml: iframeBindingHtml + }; +}); \ No newline at end of file diff --git a/cms/static/js/views/abstract_editor.js b/cms/static/js/views/abstract_editor.js index ee44649e1c..4c8b87c4e3 100644 --- a/cms/static/js/views/abstract_editor.js +++ b/cms/static/js/views/abstract_editor.js @@ -1,6 +1,6 @@ -define(["backbone", "underscore"], function(Backbone, _) { - var AbstractEditor = Backbone.View.extend({ +define(["js/views/baseview", "underscore"], function(BaseView, _) { + var AbstractEditor = BaseView.extend({ // Model is MetadataModel initialize : function() { diff --git a/cms/static/js/views/asset.js b/cms/static/js/views/asset.js index 8c3c6f3498..b6fd8fc8a6 100644 --- a/cms/static/js/views/asset.js +++ b/cms/static/js/views/asset.js @@ -1,6 +1,6 @@ -define(["backbone", "underscore", "gettext", "js/views/feedback_prompt", "js/views/feedback_notification"], - function(Backbone, _, gettext, PromptView, NotificationView) { -var AssetView = Backbone.View.extend({ +define(["js/views/baseview", "underscore", "gettext", "js/views/feedback_prompt", "js/views/feedback_notification"], + function(BaseView, _, gettext, PromptView, NotificationView) { +var AssetView = BaseView.extend({ initialize: function() { this.template = _.template($("#asset-tpl").text()); this.listenTo(this.model, "change:locked", this.updateLockState); diff --git a/cms/static/js/views/assets.js b/cms/static/js/views/assets.js index c7fe95b745..551826020b 100644 --- a/cms/static/js/views/assets.js +++ b/cms/static/js/views/assets.js @@ -1,6 +1,6 @@ -define(["backbone", "js/views/asset"], function(Backbone, AssetView) { +define(["js/views/baseview", "js/views/asset"], function(BaseView, AssetView) { -var AssetsView = Backbone.View.extend({ +var AssetsView = BaseView.extend({ // takes AssetCollection as model initialize : function() { diff --git a/cms/static/js/views/baseview.js b/cms/static/js/views/baseview.js new file mode 100644 index 0000000000..428070e7c1 --- /dev/null +++ b/cms/static/js/views/baseview.js @@ -0,0 +1,44 @@ +define( + [ + 'jquery', + 'underscore', + 'backbone', + "js/utils/handle_iframe_binding" + ], + function ($, _, Backbone, IframeUtils) { + /* This view is extended from backbone with custom functions 'beforeRender' and 'afterRender'. It allows other + views, which extend from it to access these custom functions. 'afterRender' function of BaseView calls a utility + function 'iframeBinding' which modifies iframe src urls on a page so that they are rendered as part of the DOM. + Other common functions which need to be run before/after can also be added here. + */ + + var BaseView = Backbone.View.extend({ + //override the constructor function + constructor: function(options) { + _.bindAll(this, 'beforeRender', 'render', 'afterRender'); + var _this = this; + this.render = _.wrap(this.render, function (render) { + _this.beforeRender(); + render(); + _this.afterRender(); + return _this; + }); + + //call Backbone's own constructor + Backbone.View.prototype.constructor.apply(this, arguments); + }, + + beforeRender: function () { + }, + + render: function () { + return this; + }, + + afterRender: function () { + IframeUtils.iframeBinding(this); + } + }); + + return BaseView; +}); \ No newline at end of file diff --git a/cms/static/js/views/checklist.js b/cms/static/js/views/checklist.js index be2f518942..c7af95b2a9 100644 --- a/cms/static/js/views/checklist.js +++ b/cms/static/js/views/checklist.js @@ -1,5 +1,5 @@ -define(["backbone", "underscore", "jquery"], function(Backbone, _, $) { - var ChecklistView = Backbone.View.extend({ +define(["js/views/baseview", "underscore", "jquery"], function(BaseView, _, $) { + var ChecklistView = BaseView.extend({ // takes CMS.Models.Checklists as model events : { diff --git a/cms/static/js/views/course_info_edit.js b/cms/static/js/views/course_info_edit.js index bd4dc97613..0ae932630b 100644 --- a/cms/static/js/views/course_info_edit.js +++ b/cms/static/js/views/course_info_edit.js @@ -1,12 +1,12 @@ -define(["backbone", "js/views/course_info_update", "js/views/course_info_handout"], - function(Backbone, CourseInfoUpdateView, CourseInfoHandoutView) { +define(["js/views/baseview", "js/views/course_info_update", "js/views/course_info_handout"], + function(BaseView, CourseInfoUpdateView, CourseInfoHandoutView) { /* this view should own everything on the page which has controls effecting its operation generate other views for the individual editors. The render here adds views for each update/handout by delegating to their collections but does not generate any html for the surrounding page. */ -var CourseInfoEdit = Backbone.View.extend({ +var CourseInfoEdit = BaseView.extend({ // takes CMS.Models.CourseInfo as model tagName: 'div', diff --git a/cms/static/js/views/course_info_handout.js b/cms/static/js/views/course_info_handout.js index 9309deda1b..237e879128 100644 --- a/cms/static/js/views/course_info_handout.js +++ b/cms/static/js/views/course_info_handout.js @@ -1,8 +1,8 @@ -define(["backbone", "underscore", "codemirror", "js/views/feedback_notification", "js/views/course_info_helper", "js/utils/modal"], - function(Backbone, _, CodeMirror, NotificationView, CourseInfoHelper, ModalUtils) { +define(["js/views/baseview", "underscore", "codemirror", "js/views/feedback_notification", "js/views/course_info_helper", "js/utils/modal"], + function(BaseView, _, CodeMirror, NotificationView, CourseInfoHelper, ModalUtils) { // the handouts view is dumb right now; it needs tied to a model and all that jazz - var CourseInfoHandoutsView = Backbone.View.extend({ + var CourseInfoHandoutsView = BaseView.extend({ // collection is CourseUpdateCollection events: { "click .save-button" : "onSave", diff --git a/cms/static/js/views/course_info_helper.js b/cms/static/js/views/course_info_helper.js index fb3474cdb0..3b519ec8f2 100644 --- a/cms/static/js/views/course_info_helper.js +++ b/cms/static/js/views/course_info_helper.js @@ -1,5 +1,5 @@ -define(["codemirror", "utility"], - function(CodeMirror) { +define(["codemirror", 'js/utils/handle_iframe_binding', "utility"], + function(CodeMirror, IframeBinding) { var editWithCodeMirror = function(model, contentName, baseAssetUrl, textArea) { var content = rewriteStaticLinks(model.get(contentName), baseAssetUrl, '/static/'); model.set(contentName, content); @@ -18,6 +18,11 @@ define(["codemirror", "utility"], var changeContentToPreview = function (model, contentName, baseAssetUrl) { var content = rewriteStaticLinks(model.get(contentName), '/static/', baseAssetUrl); + // Modify iframe (add wmode=transparent in url querystring) and embed (add wmode=transparent as attribute) + // tags in html string (content) so both tags will attach to dom and don't create z-index problem for other popups + // Note: content is modified before assigning to model because embed tags should be modified before rendering + // as they are static objects as compared to iframes + content = IframeBinding.iframeBindingHtml(content); model.set(contentName, content); return content; }; diff --git a/cms/static/js/views/course_info_update.js b/cms/static/js/views/course_info_update.js index d5dd82376e..821d12fcaa 100644 --- a/cms/static/js/views/course_info_update.js +++ b/cms/static/js/views/course_info_update.js @@ -1,8 +1,8 @@ -define(["backbone", "underscore", "codemirror", "js/models/course_update", +define(["js/views/baseview", "underscore", "codemirror", "js/models/course_update", "js/views/feedback_prompt", "js/views/feedback_notification", "js/views/course_info_helper", "js/utils/modal"], - function(Backbone, _, CodeMirror, CourseUpdateModel, PromptView, NotificationView, CourseInfoHelper, ModalUtils) { + function(BaseView, _, CodeMirror, CourseUpdateModel, PromptView, NotificationView, CourseInfoHelper, ModalUtils) { - var CourseInfoUpdateView = Backbone.View.extend({ + var CourseInfoUpdateView = BaseView.extend({ // collection is CourseUpdateCollection events: { "click .new-update-button" : "onNew", diff --git a/cms/static/js/views/edit_chapter.js b/cms/static/js/views/edit_chapter.js index af6c5c61dd..120240adca 100644 --- a/cms/static/js/views/edit_chapter.js +++ b/cms/static/js/views/edit_chapter.js @@ -1,7 +1,7 @@ -define(["backbone", "underscore", "underscore.string", "jquery", "gettext", "js/models/uploads", "js/views/uploads"], - function(Backbone, _, str, $, gettext, FileUploadModel, UploadDialogView) { +define(["js/views/baseview", "underscore", "underscore.string", "jquery", "gettext", "js/models/uploads", "js/views/uploads"], + function(BaseView, _, str, $, gettext, FileUploadModel, UploadDialogView) { _.str = str; // used in template - var EditChapter = Backbone.View.extend({ + var EditChapter = BaseView.extend({ initialize: function() { this.template = _.template($("#edit-chapter-tpl").text()); this.listenTo(this.model, "change", this.render); diff --git a/cms/static/js/views/edit_textbook.js b/cms/static/js/views/edit_textbook.js index 49f396391d..ef72bc5074 100644 --- a/cms/static/js/views/edit_textbook.js +++ b/cms/static/js/views/edit_textbook.js @@ -1,6 +1,6 @@ -define(["backbone", "underscore", "jquery", "js/views/edit_chapter", "js/views/feedback_notification"], - function(Backbone, _, $, EditChapterView, NotificationView) { - var EditTextbook = Backbone.View.extend({ +define(["js/views/baseview", "underscore", "jquery", "js/views/edit_chapter", "js/views/feedback_notification"], + function(BaseView, _, $, EditChapterView, NotificationView) { + var EditTextbook = BaseView.extend({ initialize: function() { this.template = _.template($("#edit-textbook-tpl").text()); this.listenTo(this.model, "invalid", this.render); diff --git a/cms/static/js/views/feedback.js b/cms/static/js/views/feedback.js index ad50d284ea..348b16e62d 100644 --- a/cms/static/js/views/feedback.js +++ b/cms/static/js/views/feedback.js @@ -1,5 +1,5 @@ -define(["backbone", "underscore", "underscore.string", "jquery"], function(Backbone, _, str, $) { - var SystemFeedback = Backbone.View.extend({ +define(["js/views/baseview", "underscore", "underscore.string", "jquery"], function(BaseView, _, str, $) { + var SystemFeedback = BaseView.extend({ options: { title: "", message: "", diff --git a/cms/static/js/views/list_textbooks.js b/cms/static/js/views/list_textbooks.js index 8911866075..3f76470706 100644 --- a/cms/static/js/views/list_textbooks.js +++ b/cms/static/js/views/list_textbooks.js @@ -1,6 +1,6 @@ -define(["backbone", "underscore", "jquery", "js/views/edit_textbook", "js/views/show_textbook"], - function(Backbone, _, $, EditTextbookView, ShowTextbookView) { - var ListTextbooks = Backbone.View.extend({ +define(["js/views/baseview", "underscore", "jquery", "js/views/edit_textbook", "js/views/show_textbook"], + function(BaseView, _, $, EditTextbookView, ShowTextbookView) { + var ListTextbooks = BaseView.extend({ initialize: function() { this.emptyTemplate = _.template($("#no-textbooks-tpl").text()); this.listenTo(this.collection, 'all', this.render); diff --git a/cms/static/js/views/metadata.js b/cms/static/js/views/metadata.js index fc9bcdd922..2f07ab117f 100644 --- a/cms/static/js/views/metadata.js +++ b/cms/static/js/views/metadata.js @@ -1,12 +1,12 @@ define( [ - "backbone", "underscore", "js/models/metadata", "js/views/abstract_editor", + "js/views/baseview", "underscore", "js/models/metadata", "js/views/abstract_editor", "js/views/transcripts/metadata_videolist" ], -function(Backbone, _, MetadataModel, AbstractEditor, VideoList) { +function(BaseView, _, MetadataModel, AbstractEditor, VideoList) { var Metadata = {}; - Metadata.Editor = Backbone.View.extend({ + Metadata.Editor = BaseView.extend({ // Model is CMS.Models.MetadataCollection, initialize : function() { diff --git a/cms/static/js/views/overview_assignment_grader.js b/cms/static/js/views/overview_assignment_grader.js index b7b501f572..b13181b5e4 100644 --- a/cms/static/js/views/overview_assignment_grader.js +++ b/cms/static/js/views/overview_assignment_grader.js @@ -1,6 +1,6 @@ -define(["backbone", "underscore", "gettext", "js/models/assignment_grade", "js/views/feedback_notification"], - function(Backbone, _, gettext, AssignmentGrade, NotificationView) { - var OverviewAssignmentGrader = Backbone.View.extend({ +define(["js/views/baseview", "underscore", "gettext", "js/models/assignment_grade", "js/views/feedback_notification"], + function(BaseView, _, gettext, AssignmentGrade, NotificationView) { + var OverviewAssignmentGrader = BaseView.extend({ // instantiate w/ { graders : CourseGraderCollection, el : } events : { "click .menu-toggle" : "showGradeMenu", diff --git a/cms/static/js/views/section_edit.js b/cms/static/js/views/section_edit.js index d8e3ed0d69..14fee5994f 100644 --- a/cms/static/js/views/section_edit.js +++ b/cms/static/js/views/section_edit.js @@ -1,5 +1,5 @@ -define(["backbone", "underscore", "js/views/feedback_prompt", "js/views/section_show", "require"], function(Backbone, _, PromptView, SectionShowView, require) { - var SectionEdit = Backbone.View.extend({ +define(["js/views/baseview", "underscore", "js/views/feedback_prompt", "js/views/section_show", "require"], function(BaseView, _, PromptView, SectionShowView, require) { + var SectionEdit = BaseView.extend({ render: function() { var attrs = { name: this.model.escape('name') diff --git a/cms/static/js/views/section_show.js b/cms/static/js/views/section_show.js index da6871336b..802826d0d2 100644 --- a/cms/static/js/views/section_show.js +++ b/cms/static/js/views/section_show.js @@ -1,6 +1,6 @@ -define(["backbone", "underscore", "gettext", "js/views/section_edit", "require"], function(Backbone, _, gettext, SectionEditView, require) { +define(["js/views/baseview", "underscore", "gettext", "js/views/section_edit", "require"], function(BaseView, _, gettext, SectionEditView, require) { - var SectionShow = Backbone.View.extend({ + var SectionShow = BaseView.extend({ template: _.template('" class="section-name-span"><%= name %>'), render: function() { var attrs = { diff --git a/cms/static/js/views/show_textbook.js b/cms/static/js/views/show_textbook.js index 580f4e9840..2483d6dcc3 100644 --- a/cms/static/js/views/show_textbook.js +++ b/cms/static/js/views/show_textbook.js @@ -1,6 +1,6 @@ -define(["backbone", "underscore", "gettext", "js/views/feedback_notification", "js/views/feedback_prompt"], - function(Backbone, _, gettext, NotificationView, PromptView) { - var ShowTextbook = Backbone.View.extend({ +define(["js/views/baseview", "underscore", "gettext", "js/views/feedback_notification", "js/views/feedback_prompt"], + function(BaseView, _, gettext, NotificationView, PromptView) { + var ShowTextbook = BaseView.extend({ initialize: function() { this.template = _.template($("#show-textbook-tpl").text()); this.listenTo(this.model, "change", this.render); diff --git a/cms/static/js/views/uploads.js b/cms/static/js/views/uploads.js index efd536a652..d09dd745c7 100644 --- a/cms/static/js/views/uploads.js +++ b/cms/static/js/views/uploads.js @@ -1,6 +1,6 @@ -define(["backbone", "underscore", "jquery", "jquery.form"], -function(Backbone, _, $) { -var UploadDialog = Backbone.View.extend({ +define(["js/views/baseview", "underscore", "jquery", "jquery.form"], +function(BaseView, _, $) { +var UploadDialog = BaseView.extend({ options: { shown: true, successMessageTimeout: 2000 // 2 seconds diff --git a/cms/static/js/views/validation.js b/cms/static/js/views/validation.js index 04b32e1ea1..bbca2e67a0 100644 --- a/cms/static/js/views/validation.js +++ b/cms/static/js/views/validation.js @@ -1,7 +1,7 @@ -define(["backbone", "underscore", "jquery", "gettext", "js/views/feedback_notification", "js/views/feedback_alert", "jquery.smoothScroll"], - function(Backbone, _, $, gettext, NotificationView, AlertView) { +define(["js/views/baseview", "underscore", "jquery", "gettext", "js/views/feedback_notification", "js/views/feedback_alert", "js/views/baseview", "jquery.smoothScroll"], + function(BaseView, _, $, gettext, NotificationView, AlertView) { -var ValidatingView = Backbone.View.extend({ +var ValidatingView = BaseView.extend({ // Intended as an abstract class which catches validation errors on the model and // decorates the fields. Needs wiring per class, but this initialization shows how // either have your init call this one or copy the contents