Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Browse files

Refactor screen page to use Backbone.js.

  • Loading branch information...
commit 788460bd55f2e5e4a965748485a749784581ada7 1 parent dae8e09
@karthikv karthikv authored
Showing with 1,072 additions and 1,081 deletions.
  1. +11 −0 public/javascripts/collections/component.js
  2. +12 −0 public/javascripts/collections/element.js
  3. +1 −5 public/javascripts/collections/project.js
  4. +0 −4 public/javascripts/collections/screen.js
  5. +30 −10 public/javascripts/helpers/shared-models.js
  6. +0 −40 public/javascripts/helpers/utils.js
  7. +2 −8 public/javascripts/models/component.js
  8. +2 −8 public/javascripts/models/element.js
  9. +15 −38 public/javascripts/models/extended.js
  10. +2 −2 public/javascripts/models/project.js
  11. +6 −2 public/javascripts/models/screen.js
  12. +2 −2 public/javascripts/scripts/index.js
  13. +9 −8 public/javascripts/scripts/prototype.js
  14. +49 −37 public/javascripts/views/component-list.js
  15. +226 −245 public/javascripts/views/component.js
  16. +56 −49 public/javascripts/views/element-list.js
  17. +208 −157 public/javascripts/views/element.js
  18. +51 −21 public/javascripts/views/extended.js
  19. +13 −0 public/javascripts/views/key-manager.js
  20. +89 −95 public/javascripts/views/layout-modification.js
  21. +46 −35 public/javascripts/views/screen-actions.js
  22. +85 −84 public/javascripts/views/screen-layout.js
  23. +7 −13 public/javascripts/views/screen-list.js
  24. +0 −23 public/javascripts/views/switch.js
  25. +0 −11 public/javascripts/views/window.js
  26. +2 −1  public/stylesheets/main.styl
  27. +6 −1 todo.txt
  28. +12 −9 views/helpers/share-login-form.jade
  29. +5 −7 views/templates/elements/auth.jade
  30. +3 −5 views/templates/elements/external-link.jade
  31. +14 −29 views/templates/elements/heading.jade
  32. +6 −9 views/templates/elements/input-checkbox.jade
  33. +6 −9 views/templates/elements/input-radio.jade
  34. +6 −9 views/templates/elements/input-text.jade
  35. +2 −5 views/templates/elements/paragraph.jade
  36. +3 −5 views/templates/elements/screen-link.jade
  37. +6 −9 views/templates/elements/textarea.jade
  38. +4 −0 views/templates/helpers/screen-selection.jade
  39. +4 −5 views/templates/layouts/layout.jade
  40. +1 −1  views/templates/lists/article-element.jade
  41. +38 −38 views/templates/lists/component.jade
  42. +3 −3 views/templates/lists/form-element.jade
  43. +2 −11 views/templates/lists/navigation-element.jade
  44. +2 −2 views/templates/popovers/auth.jade
  45. +3 −3 views/templates/popovers/external-link.jade
  46. +4 −4 views/templates/popovers/heading.jade
  47. +2 −2 views/templates/popovers/input-checkbox.jade
  48. +2 −2 views/templates/popovers/input-radio.jade
  49. +2 −2 views/templates/popovers/input-text.jade
  50. +2 −2 views/templates/popovers/paragraph.jade
  51. +3 −3 views/templates/popovers/screen-link.jade
  52. +2 −2 views/templates/popovers/textarea.jade
  53. +5 −6 views/templates/wrappers/form.jade
View
11 public/javascripts/collections/component.js
@@ -0,0 +1,11 @@
+define(['jquery', 'backbone', './extended', 'models/component'],
+ function($, Backbone, ExtendedCollection, ComponentModel) {
+ return ExtendedCollection.extend({
+ url: function() {
+ return '/projects/' + this.options.projectId + '/screens/' +
+ this.options.screenId + '/components';
+ },
+
+ model: ComponentModel
+ });
+ });
View
12 public/javascripts/collections/element.js
@@ -0,0 +1,12 @@
+define(['jquery', 'backbone', './extended', 'models/element'],
+ function($, Backbone, ExtendedCollection, ElementModel) {
+ return ExtendedCollection.extend({
+ url: function() {
+ return '/projects/' + this.options.projectId + '/screens/' +
+ this.options.screenId + '/components/' + this.options.componentId +
+ '/elements';
+ },
+
+ model: ElementModel
+ });
+ });
View
6 public/javascripts/collections/project.js
@@ -1,13 +1,9 @@
define(['jquery', 'backbone', './extended', 'models/project'],
function($, Backbone, ExtendedCollection, ProjectModel) {
return ExtendedCollection.extend({
- url: 'projects',
+ url: '/projects',
model: ProjectModel,
- initialize: function(models, options) {
- this.constructParent(arguments);
- },
-
comparator: function(project) {
return project.get('id');
}
View
4 public/javascripts/collections/screen.js
@@ -5,10 +5,6 @@ define(['jquery', 'backbone', './extended', 'models/screen'],
return '/projects/' + this.options.projectId + '/screens';
},
- initialize: function(models, options) {
- this.constructParent(arguments);
- },
-
model: ScreenModel,
comparator: function(screen) {
return screen.get('id');
View
40 public/javascripts/helpers/shared-models.js
@@ -1,25 +1,45 @@
-define(['models/screen', 'helpers/screen-utils'],
- function(ScreenModel, screenUtils) {
+define(['backbone', 'models/screen', 'helpers/screen-utils'],
+ function(Backbone, ScreenModel, screenUtils) {
+ var mediator = _.extend({}, Backbone.Events);
+
return {
/* Gets the current screen model.
* Returns: the current screen model
*/
getCurrentScreen: function() {
var urlData = screenUtils.getUrlData();
- var deferred = new can.Deferred();
+
var self = this;
+ var deferred = $.Deferred();
- if (self.cachedScreen) {
+ if (self.waitingForScreen) {
+ // this method may be called while a screen is being fetched; in
+ // this case, hook into the mediator's sharedModels:screen event,
+ // which will hold the resultant string
+ mediator.on('sharedModels:screen', function(screen) {
+ deferred.resolve(screen);
+ });
+ } else if (self.cachedScreen) {
deferred.resolve(self.cachedScreen);
} else {
- ScreenModel.withRouteData()
- .findOne({ id: urlData.screenId })
- .then(function(screen) {
+ // it takes time to get the screen, and we don't want two screen
+ // requests; as a result, set a flag here indicating the screen is on
+ // its way
+ self.waitingForScreen = true;
+ var screen = new ScreenModel({ id: urlData.screenId }, urlData);
+
+ screen.fetch({
+ success: function() {
self.cachedScreen = screen;
+ self.waitingForScreen = false;
+
+ // trigger a sharedModels:screen event when the screen has
+ // arrived for all others that may have called this method while
+ // the screen was being fetched (see mediator.on() above)
+ mediator.trigger('sharedModels:screen', screen);
deferred.resolve(screen);
- }, function(xhr) {
- deferred.reject(xhr);
- });
+ }
+ });
}
return deferred;
View
40 public/javascripts/helpers/utils.js
@@ -1,40 +0,0 @@
-define(function() {
- return {
- /* Binds the context to the given function.
- * Requires: function, context
- * Returns: function that calls the originally given function with this set
- * to the given context
- */
- bind: function(fn, context) {
- return function() {
- return fn.apply(context, Array.prototype.slice.call(arguments, 0));
- };
- },
-
- /* Debounces the given function on the trailing edge, preventing it from
- * being called until at least the given time in ms has elapsed since the
- * last call of the function.
- * Requires: function, context, debouncing time
- * Returns: debounced function
- */
- debounce: function(fn, context, time) {
- var timeoutToCall;
-
- return function() {
- var parentArgs = arguments;
-
- // if there is an active timeout, clear it and set a new one, waiting
- // until the given time has elapsed since the last call of this function
- if (timeoutToCall) {
- clearTimeout(timeoutToCall);
- }
-
- timeoutToCall = setTimeout(function() {
- // call fn with the arguments to the debounced function
- fn.apply(context, Array.prototype.slice.call(parentArgs, 0));
- timeoutToCall = null;
- }, time);
- };
- }
- };
-});
View
10 public/javascripts/models/component.js
@@ -1,9 +1,3 @@
-define(['can', './extended'], function(can, ExtendedModel) {
- return ExtendedModel({
- findAll: 'GET /projects/{projectId}/screens/{screenId}/components',
- findOne: 'GET /projects/{projectId}/screens/{screenId}/components/{id}',
- create: 'POST /projects/{projectId}/screens/{screenId}/components',
- update: 'PUT /projects/{projectId}/screens/{screenId}/components/{id}',
- destroy: 'DELETE /projects/{projectId}/screens/{screenId}/components/{id}'
- }, {});
+define(['backbone', './extended'], function(Backbone, ExtendedModel) {
+ return ExtendedModel.extend({});
});
View
10 public/javascripts/models/element.js
@@ -1,9 +1,3 @@
-define(['can', './extended'], function(can, ExtendedModel) {
- return ExtendedModel({
- findAll: 'GET /projects/{projectId}/screens/{screenId}/components/{componentId}/elements',
- findOne: 'GET /projects/{projectId}/screens/{screenId}/components/{componentId}/elements/{id}',
- create: 'POST /projects/{projectId}/screens/{screenId}/components/{componentId}/elements',
- update: 'PUT /projects/{projectId}/screens/{screenId}/components/{componentId}/elements/{id}',
- destroy: 'DELETE /projects/{projectId}/screens/{screenId}/components/{componentId}/elements/{id}'
- }, {});
+define(['backbone', './extended'], function(Backbone, ExtendedModel) {
+ return ExtendedModel.extend({});
});
View
53 public/javascripts/models/extended.js
@@ -1,45 +1,22 @@
-define(['can', 'helpers/screen-utils'], function(can, screenUtils) {
- return can.Model({
- // run findAll/findOne with URL data from screenUtils
- withRouteData: function() {
- var self = this;
- var urlData = screenUtils.getUrlData();
+define(['backbone', 'underscore'], function(Backbone, _) {
+ return Backbone.Model.extend({
+ url: function() {
+ // prioritize the collection URL first
+ var baseUrl = _.result(this.collection, 'url') || _.result(this, 'urlRoot');
- return {
- findAll: function(params) {
- var args = Array.prototype.slice.call(arguments, 0);
- args.unshift('findAll');
- return this.run.apply(this, args);
- },
-
- findOne: function(params) {
- var args = Array.prototype.slice.call(arguments, 0);
- args.unshift('findOne');
- return this.run.apply(this, args);
- },
-
- run: function(name, params) {
- var args = Array.prototype.slice.call(arguments, 2);
- params = can.extend({}, urlData, params);
- args.unshift(params);
- return self[name].apply(self, args);
- }
- };
- }
- }, {
- // run save/destory with URL data from screenUtils
- withRouteData: function(params) {
- var urlData = screenUtils.getUrlData();
- if (urlData) {
- this.attr(urlData);
+ if (this.isNew()) {
+ return baseUrl;
}
- // extra optional data provided by the user
- if (params) {
- this.attr(params);
+ // make baseUrl end with a trailing slash
+ if (baseUrl.charAt(baseUrl.length - 1) !== '/') {
+ baseUrl += '/';
}
- // for chaining purposes
- return this;
+ return baseUrl + encodeURIComponent(this.id);
+ },
+
+ initialize: function(attributes, options) {
+ this.options = options;
}
});
});
View
4 public/javascripts/models/project.js
@@ -1,3 +1,3 @@
-define(['backbone'], function(Backbone) {
- return Backbone.Model.extend({});
+define(['backbone', './extended'], function(Backbone, ExtendedModel) {
+ return ExtendedModel.extend({});
});
View
8 public/javascripts/models/screen.js
@@ -1,3 +1,7 @@
-define(['backbone'], function(Backbone) {
- return Backbone.Model.extend({});
+define(['backbone', './extended'], function(Backbone, ExtendedModel) {
+ return ExtendedModel.extend({
+ urlRoot: function() {
+ return '/projects/' + this.options.projectId + '/screens';
+ }
+ });
});
View
4 public/javascripts/scripts/index.js
@@ -7,8 +7,8 @@ require.config({
}
});
-require(['views/project-list', 'views/screen-list'],
- function(ProjectListView, ScreenListView) {
+require(['jquery', 'views/project-list', 'views/screen-list'],
+ function($, ProjectListView, ScreenListView) {
new ProjectListView({ el: $('#sidebar') });
new ScreenListView({ el: $('#content') });
});
View
17 public/javascripts/scripts/prototype.js
@@ -1,22 +1,23 @@
require.config({
baseUrl: '/javascripts/',
paths: {
- can: 'lib/can.jquery.min',
- 'can.super': 'lib/can.construct.super',
+ backbone: 'lib/backbone.min',
+ underscore: 'lib/underscore.min',
+ router: 'routers/project-page',
'jquery.ui': 'lib/jquery.ui',
'jquery.serialize': 'lib/jquery.serialize'
}
});
-require(['controllers/window', 'controllers/screen-layout',
- 'controllers/screen-actions', 'helpers/screen-utils'],
- function(WindowControl, ScreenLayoutControl, ScreenActionsControl, screenUtils) {
- new WindowControl(window, {});
- new ScreenLayoutControl('#content', {});
+require(['jquery', 'views/key-manager', 'views/screen-layout',
+ 'views/screen-actions', 'helpers/screen-utils'],
+ function($, KeyManagerView, ScreenLayoutView, ScreenActionsView, screenUtils) {
+ new KeyManagerView({ el: window });
+ new ScreenLayoutView({ el: $('#content') });
if (!screenUtils.isSharePage()) {
// screen actions should only be present on the prototyping page
- new ScreenActionsControl('#screen-actions', {});
+ new ScreenActionsView({ el: $('#screen-actions') });
} else {
// adds BrowserID functionality to sign in buttons
require(['scripts/share-login']);
View
86 public/javascripts/views/component-list.js
@@ -1,17 +1,17 @@
-define(['jquery', 'can', './switch', 'helpers/shared-models', 'helpers/errors',
- './layout-modification', 'can.super', 'jquery.serialize', 'jquery.ui'],
- function($, can, SwitchControl, sharedModels, errors, LayoutModificationControl) {
- return SwitchControl({
- init: function($element, options) {
- this._super($element, options);
+define(['jquery', 'backbone', 'underscore', './extended', 'helpers/shared-models',
+ 'helpers/errors', './layout-modification', 'jquery.serialize', 'jquery.ui'],
+ function($, Backbone, _, ExtendedView, sharedModels, errors, LayoutModificationView) {
+ return ExtendedView.extend({
+ template: _.template($('#component-list-template').html()),
+
+ initialize: function(options) {
+ this.constructParent(arguments);
var self = this;
sharedModels.getCurrentScreen()
.then(function(screen) {
self.screen = screen;
- self.activate();
- }, function() {
- // TODO: handle error
+ self.render();
});
},
@@ -22,43 +22,55 @@ define(['jquery', 'can', './switch', 'helpers/shared-models', 'helpers/errors',
},
render: function() {
- this.element.html(can.view('component-list-template', this.screen));
+ this.$el.html(this.template(this.screen.toJSON()));
this.$('.component').draggable(this.dragOptions);
// to control screen layout modifications
- new LayoutModificationControl(this.$('#layout-modifications'), {});
- this.$('.dropdown-toggle').dropdown();
+ var view = new LayoutModificationView({ screen: this.screen });
+ this.$el.append(view.render().el);
},
- '#screen-config submit': function($form, event) {
- event.preventDefault();
- var $submit = $form.find('[type="submit"]');
- $submit.attr('disabled', 'disabled');
+ unrender: function() {
+ // nothing to do
+ },
- var formData = $form.serializeObject();
- // secure will not be set to false if the checkbox is not checked;
- // instead, it will remain undefined; in this case, set it manually
- if (!formData.secure) {
- formData.secure = false;
- }
+ events: {
+ 'submit #screen-config': function(event) {
+ event.preventDefault();
+ var $form = $(event.currentTarget);
- // merge form data with screen attributes
- this.screen.attr(formData);
- this.screen.withRouteData()
- .save()
- .then(function(screen) {
- // visual feedback with a check icon
- var $check = $form.find('.icon-ok');
- $check.show();
+ var $submit = $form.find('[type="submit"]');
+ $submit.attr('disabled', 'disabled');
+
+ var formData = $form.serializeObject();
+ // secure will not be set to false if the checkbox is not checked;
+ // instead, it will remain undefined; in this case, set it manually
+ if (!formData.secure) {
+ formData.secure = false;
+ }
- $submit.removeAttr('disabled');
- setTimeout(function() {
- $check.hide();
- }, 1000);
- }, function(xhr) {
- $submit.removeAttr('disabled');
- errors.tooltipHandler($submit)(xhr);
+ // merge form data with screen attributes
+ this.screen.save(formData, {
+ success: function() {
+ // visual feedback with a check icon
+ var $check = $form.find('.icon-ok');
+ $check.show();
+
+ $submit.removeAttr('disabled');
+ setTimeout(function() {
+ $check.hide();
+ }, 1000);
+ },
+
+ error: function() {
+ var args = Array.prototype.slice.call(arguments, 0);
+ $submit.removeAttr('disabled');
+ errors.tooltipHandler($submit).apply(errors, args);
+ },
+
+ wait: true
});
+ }
}
});
});
View
471 public/javascripts/views/component.js
@@ -1,26 +1,28 @@
-define(['can', './extended', './element', 'models/element', 'helpers/screen-utils',
- 'helpers/errors', 'can.super', 'jquery.ui'],
- function(can, ExtendedControl, ElementControl, ElementModel, screenUtils, errors) {
- return ExtendedControl({
- fetchingElements: null
- }, {
- init: function($element, options) {
- this._super($element, options);
- this.component = options.component;
-
- // used for event handlers
- this.off();
- this.options.content = $('#content');
- this.options.$sidebar = $('#sidebar');
- this.on();
+define(['jquery', 'backbone', 'underscore', './extended', 'collections/element',
+ './element', 'helpers/screen-utils', 'helpers/errors', 'jquery.ui'],
+ function($, Backbone, _, ExtendedView, ElementCollection, ElementView,
+ screenUtils, errors) {
+ return ExtendedView.extend({
+ template: _.template($('#component-action-template').html()),
+
+ initialize: function(options) {
+ this.constructParent(arguments);
+
+ this.$el.addClass('has-component');
+ this.$el.addClass(this.model.get('type') + '-container');
- var type = this.getType();
- this.element.addClass('has-component');
- this.element.addClass(type + '-container');
+ var urlData = screenUtils.getUrlData();
+ this.Elements = new ElementCollection([],
+ _.extend({ componentId: this.model.get('id') }, urlData));
- this.addAllElements();
+ this.Elements.on('reset', this.addAllElements, this);
+ this.Elements.on('remove', this.addAllElements, this);
+
+ this.Elements.fetch();
+
+ // sortable only on prototype page
if (!screenUtils.isSharePage()) {
- this.element.sortable(this.sortableOptions);
+ this.$el.sortable(this.sortableOptions);
}
},
@@ -28,303 +30,282 @@ define(['can', './extended', './element', 'models/element', 'helpers/screen-util
items: '.live-element'
},
- getType: function() {
- return this.component.attr('type');
- },
-
addAllElements: function() {
var self = this;
- var componentId = self.component.attr('id');
-
- if (self.cachedElements) {
- self.traverseElementLinkedList();
- } else {
- if (this.constructor.fetchingElements) {
- // pipe all element retrieval into one deferred to prevent
- // simultaneous fetching, as this causes a bug in CanJS
- this.constructor.fetchingElements = this.constructor
- .fetchingElements.pipe(getElements);
- } else {
- this.constructor.fetchingElements = getElements();
- }
- }
-
- function getElements() {
- // TODO: optimize this when layout row is removed (new request for
- // every moved component)
- return ElementModel.withRouteData()
- .findAll({ componentId: componentId })
- .then(function(elements) {
- self.cachedElements = elements;
- self.traverseElementLinkedList();
- }, function(xhr) {
- // TODO: handle error
- });
- }
- },
-
- setComponentEmpty: function(empty) {
- // clear out leftover elements and add component actions
- this.element.html(can.view('component-action-template', {}));
-
- if (empty) {
- this.element.addClass('empty');
- // no elements; add a type label as a placeholder
- this.element.append(this.getType());
- } else {
- this.element.removeClass('empty');
- }
- },
-
- traverseElementLinkedList: function() {
- var self = this;
var curElement;
// id => element map
var idElementMap = {};
- var elements = self.cachedElements;
-
- var elementsContainer = self.element;
+ var $elementsContainer = self.$el;
var originalContainerHTML;
- elements.each(function(element, index) {
- idElementMap[element.attr('id')] = element;
+ self.Elements.each(function(element, index) {
+ idElementMap[element.get('id')] = element;
// begin with the head
- if (element.attr('head')) {
+ if (element.get('head')) {
curElement = element;
}
});
// if there are elements to add
if (curElement) {
- if (self.element.hasClass('empty')) {
- // component is no longer empty
- self.setComponentEmpty(false);
- }
+ // component is no longer empty
+ self.setComponentEmpty(false);
+
+ var wrapperId = self.model.get('type') + '-wrapper-template';
+ var $wrapper = $('#' + wrapperId);
- var wrapperId = self.component.attr('type') + '-wrapper-template';
// if the wrapper template exists, add it to this component
- if ($('#' + wrapperId).length === 1) {
- self.element.append(can.view(wrapperId, self.component));
+ if ($wrapper.length === 1) {
+ var wrapperTemplate = _.template($wrapper.html());
+ self.$el.append(wrapperTemplate(self.model.toJSON()));
// where the elements should be added to
- elementsContainer = self.$('.elements-container');
+ $elementsContainer = self.$('.elements-container');
// clear out the container, but save its contents to be added after
// all of the elements
- originalContainerHTML = elementsContainer.html();
- elementsContainer.html('');
+ originalContainerHTML = $elementsContainer.html();
+ $elementsContainer.html('');
}
+ } else {
+ self.setComponentEmpty(true);
}
+ var index = 0;
while (curElement) {
// go through the linked list via the nextId attribute, adding each
// element one-by-one
- self.addElement(curElement, elementsContainer);
- curElement = idElementMap[curElement.attr('nextId')];
+ self.addElement(curElement, $elementsContainer);
+ curElement = idElementMap[curElement.get('nextId')];
+
+ if (++index === 100) {
+ console.log('This seems like an infinite loop due to a circular linked list.');
+ break;
+ }
}
if (originalContainerHTML) {
- elementsContainer.append(originalContainerHTML);
+ $elementsContainer.append(originalContainerHTML);
}
},
- addElement: function(element, container) {
- var type = element.attr('type');
- new ElementControl(this.element, {
- elementModel: element,
- container: container,
- component: this.component
+ addElement: function(element, $container) {
+ var type = element.get('type');
+ var view = new ElementView({
+ model: element,
+ component: this.model
});
- },
- remove: function() {
- var self = this;
- self.element.removeClass('has-component');
- self.element.removeClass(this.getType() + '-container');
-
- self.deselect(true);
- self.element.empty();
- self.element.sortable('destroy');
-
- self.component.withRouteData()
- .destroy()
- .then(function(component) {
- self.destroy();
- }, function(xhr) {
- // TODO: handle error
- });
+ $container.append(view.render().el);
},
- select: function() {
- if (!this.element.hasClass('active')) {
- this.element.addClass('active');
- this.element.trigger('selected', this.component);
+ setComponentEmpty: function(empty) {
+ // clear out leftover elements and add component actions
+ this.$el.html(this.template(this.model.toJSON()));
+
+ if (empty) {
+ this.$el.addClass('empty');
+ // no elements; add a type label as a placeholder
+ this.$el.append(this.model.get('type'));
+ } else {
+ this.$el.removeClass('empty');
}
},
- deselect: function(triggerDeselectedAll) {
- if (this.element.hasClass('active')) {
- this.element.removeClass('active');
- this.element.trigger('deselected', this.component);
+ remove: function() {
+ this.$el.removeClass('has-component');
+ this.$el.removeClass(this.model.get('type') + '-container');
- if (triggerDeselectedAll) {
- this.options.content.trigger('deselectedAll', this.component);
- }
- }
- },
+ this.deselect(true);
+ this.$el.empty();
- getLastElement: function() {
- return this.$('.live-element').last()
- .data('model');
- },
+ this.undelegateEvents();
+ this.trigger('remove');
+ this.off();
- '{content} .component-location sortstart': function($componentLocation, event, ui) {
- if ($componentLocation.is(this.element)) {
- this.select();
- var $element = $(ui.item);
- var elementControl = $element.data('controls')[0];
- elementControl.removeElementFromLinkedList();
+ this.$el.sortable('destroy');
+ this.model.destroy();
+ },
- this.originalPrev = $element.prev();
- this.options.content.trigger('deactivateElementRequested');
+ select: function() {
+ if (!this.$el.hasClass('active')) {
+ this.$el.addClass('active');
+ this.publish('component:selected', this.model);
}
},
- '{content} .component-location sortstop': function($componentLocation, event, ui) {
- if ($componentLocation.is(this.element)) {
- var $element = $(ui.item);
- var elementControl = $element.data('controls')[0];
- elementControl.updateElementPosition();
+ deselect: function(triggerDeselectedAll) {
+ if (this.$el.hasClass('active')) {
+ this.$el.removeClass('active');
+ this.publish('component:deselected', this.model);
- if (this.originalPrev.is($element.prev())) {
- // element wasn't moved; treat this as a click and activate the popover
- elementControl.activate();
+ if (triggerDeselectedAll) {
+ this.publish('component:deselectedAll', this.model);
}
}
},
- '{content} click': function($element, event) {
- this.deselect(true);
+ getLastElement: function() {
+ var $lastElement = this.$('.live-element').last();
+ return $lastElement.data('model');
},
- '{content} .component-location click': function($element, event) {
- if ($element.is(this.element)) {
- // stop propagation to prevent {content} click handler above that blurs
- // all components
- event.stopPropagation();
- this.select();
- } else {
- // some other component was clicked; deselect this one
- // also deselect all if the element that was clicked does not have
- // a component associated with it
- this.deselect(!$element.hasClass('has-component'));
- }
+ events: {
+ 'click .icon-trash': function(event) {
+ event.preventDefault();
+ this.publish('component:deactivateElements');
+ this.remove();
+ },
},
- '{$sidebar} .element addRequested': function($element, event) {
- if (this.element.hasClass('active')) {
- var self = this;
- // gather all element data
- var data = $element.data();
+ contextualEvents: {
+ 'sortstart #content | .component-location': function(event, ui) {
+ var $componentLocation = $(event.currentTarget);
+ if ($componentLocation.is(this.$el)) {
+ this.select();
- // integer level for headings
- if (data.level) {
- data.level = parseInt(data.level, 10);
- }
+ var $element = $(ui.item);
+ this.$originalPrev = $element.prev();
- // text for headings/paragraphs
- var text = $element.text();
- if (text) {
- data.text = text;
+ this.publish('component:removeElement', $element);
+ this.publish('component:deactivateElements');
}
-
- var element = new ElementModel(data);
- var lastElement = self.getLastElement();
-
- if (!lastElement) {
- element.attr('head', true);
+ },
+
+ 'sortstop #content | .component-location': function(event, ui) {
+ var $componentLocation = $(event.currentTarget);
+ if ($componentLocation.is(this.$el)) {
+ var $element = $(ui.item);
+ this.publish('component:insertElement', $element);
+
+ if (this.$originalPrev.is($element.prev())) {
+ // element wasn't moved; treat this as a click and activate the popover
+ this.publish('component:activateElement', $element);
+ }
}
+ },
+
+ 'click window | #content': function(event) {
+ this.deselect(true);
+ },
+
+ 'click #content | .component-location': function(event) {
+ var $componentLocation = $(event.currentTarget);
+ if ($componentLocation.is(this.$el)) {
+ // stop propagation to prevent content click handler above that blurs
+ // all components
+ event.stopPropagation();
+ this.select();
+ } else {
+ // some other component was clicked; deselect this one
+ // also deselect all if the location that was clicked does not have
+ // a component associated with it
+ this.deselect(!$componentLocation.hasClass('has-component'));
+ }
+ },
- // TODO: how to factor out componentId from this as well?
- element.withRouteData({ componentId: self.component.attr('id') })
- .save()
- .then(function(element) {
- element.justCreated = true;
-
- // add element to this component
- function addElement() {
- self.cachedElements.push(element);
- self.setComponentEmpty(true);
- self.traverseElementLinkedList();
- }
-
- if (lastElement) {
- // lastElement is no longer the last; it now has a next
- lastElement.attr('nextId', element.attr('id'));
- lastElement.withRouteData({ componentId: self.component.attr('id') })
- .save()
- .then(addElement);
- } else {
- addElement();
- }
- }, errors.tooltipHandler($element));
- }
- },
-
- '{$sidebar} #back click': function($element, event) {
- this.deselect(true);
- },
-
- '.icon-trash click': function($element, event) {
- event.preventDefault();
- this.options.content.trigger('deactivateElementRequested');
- this.remove();
- },
-
- '{content} deleteComponentRequested': function($element, event, location) {
- // remove this component if it corresponds to the given location
- if (location.row === this.component.attr('row') && location.col ===
- this.component.attr('col')) {
- this.remove();
+ 'click #sidebar | #back': function(event) {
+ this.deselect(true);
}
},
- '{content} deleteRowRequested': function($element, event, row) {
- var componentRow = this.component.attr('row');
-
- // remove this component if it is in the given row
- if (componentRow === row) {
- this.remove();
- }
-
- // move this component up one row if it is below the given row
- if (componentRow > row) {
- this.component.attr('row', componentRow - 1);
- this.component.withRouteData()
- .save()
- .then(function(component) {
- }, function(xhr) {
- // TODO: handle error
+ subscriptions: {
+ 'screenLayout:selectComponent': function(component) {
+ if (this.model === component) {
+ this.select();
+ }
+ },
+
+ 'elementList:addElement': function($element) {
+ if (this.$el.hasClass('active')) {
+ var self = this;
+ // gather all element data; clone to prevent data modifications below
+ // from affecting the original element
+ var data = _.extend({}, $element.data());
+
+ // integer level for headings
+ if (data.level) {
+ data.level = parseInt(data.level, 10);
+ }
+
+ // text for headings/paragraphs
+ var text = $element.text();
+ if (text) {
+ data.text = text;
+ }
+
+ var lastElement = self.getLastElement();
+ if (!lastElement) {
+ data.head = true;
+ }
+
+ self.Elements.create(data, {
+ success: function(element) {
+ element.justCreated = true;
+
+ function onLinkedListReady() {
+ // when the linked list ready, refresh the elements
+ self.publish('component:deactivateElements');
+ self.addAllElements();
+ }
+
+ if (lastElement) {
+ // lastElement is no longer the last; it now has a next
+ lastElement.save({ nextId: element.get('id') }, {
+ success: onLinkedListReady,
+ wait: true
+ });
+ } else {
+ onLinkedListReady();
+ }
+ },
+
+ error: errors.tooltipHandler($element),
+ wait: true
});
- }
- },
+ }
+ },
- '{window} isolatedKeyDown': function($window, event, keyEvent) {
- // only activate keyboard shortcuts if component is at the front of
- // focus; i.e. no element is active
- if (this.element.hasClass('active') && !this.$('.live-element').hasClass('active')) {
- // escape key
- if (keyEvent.which === 27) {
- this.deselect(true);
+ 'layoutModification:deleteComponent': function(location) {
+ // remove this component if it corresponds to the given location
+ if (location.row === this.model.get('row') && location.col ===
+ this.model.get('col')) {
+ this.remove();
}
+ },
+
+ 'layoutModification:deleteRow': function(row) {
+ var componentRow = this.model.get('row');
- // backspace key
- if (keyEvent.which === 8) {
- keyEvent.preventDefault();
+ // remove this component if it is in the given row
+ if (componentRow === row) {
this.remove();
}
+
+ // move this component up one row if it is below the given row
+ if (componentRow > row) {
+ this.model.save({ row: componentRow - 1}, { wait: true });
+ }
+ },
+
+ 'keyManager:keyDown': function(keyEvent) {
+ // only activate keyboard shortcuts if component is at the front of
+ // focus; i.e. no element is active
+ if (this.$el.hasClass('active') &&
+ !this.$('.live-element').hasClass('active')) {
+ // escape key
+ if (keyEvent.which === 27) {
+ this.deselect(true);
+ }
+
+ // backspace key
+ if (keyEvent.which === 8) {
+ keyEvent.preventDefault();
+ this.remove();
+ }
+ }
}
}
});
View
105 public/javascripts/views/element-list.js
@@ -1,30 +1,29 @@
-define(['jquery', 'can', './switch', 'helpers/errors', 'can.super', 'jquery.serialize'],
- function($, can, SwitchControl, errors) {
- return SwitchControl({
- init: function($element, options) {
- this._super($element, options);
+define(['jquery', 'backbone', 'underscore', './extended', 'helpers/errors',
+ 'jquery.serialize'],
+ function($, Backbone, _, ExtendedView, errors) {
+ return ExtendedView.extend({
+ initialize: function(options) {
+ this.constructParent(arguments);
this.setComponentModel(options.component);
- },
-
- deactivate: function() {
- this.unrender();
- this._super();
+ this.render();
},
setComponentModel: function(component) {
if (this.component !== component) {
- this.deactivate();
+ this.publish('screenActions:deactivateElementsInComponent', this.component);
this.component = component;
- this.activate();
}
},
render: function() {
- var type = this.component.attr('type');
- this.element.html(can.view(type + '-element-list-template', this.component));
+ var type = this.component.get('type');
+ var templateId = type + '-element-list-template';
+
+ var template = _.template($('#' + templateId).html());
+ this.$el.html(template(this.component.toJSON()));
- this.element.addClass('elements');
- this.element.addClass(type + '-elements');
+ this.$el.addClass('elements');
+ this.$el.addClass(type + '-elements');
this.centerAddButtons();
// prevent links/inputs on sidebar from being tabbed to so that user
@@ -39,9 +38,9 @@ define(['jquery', 'can', './switch', 'helpers/errors', 'can.super', 'jquery.seri
unrender: function() {
if (this.component) {
- var type = this.component.attr('type');
- this.element.removeClass('elements');
- this.element.removeClass(type + '-elements');
+ var type = this.component.get('type');
+ this.$el.removeClass('elements');
+ this.$el.removeClass(type + '-elements');
}
},
@@ -51,43 +50,51 @@ define(['jquery', 'can', './switch', 'helpers/errors', 'can.super', 'jquery.seri
var $element = $btn.siblings('.element');
if ($element.height() > $btn.height()) {
- // center each button vertically if the parent element is larger
- // than it
+ // center each button vertically if the parent element is larger than it
$btn.css('margin-top', ($element.height() - $btn.height()) / 2);
}
});
},
- '.component-config submit': function($form, event) {
- event.preventDefault();
- var $submit = $form.find('[type="submit"]');
- $submit.attr('disabled', 'disabled');
-
- var formData = $form.serializeObject();
- // merge form data with component attributes
- this.component.attr(formData);
-
- this.component.withRouteData()
- .save()
- .then(function(component) {
- // visual feedback with a check icon
- var $check = $form.find('.icon-ok');
- $check.show();
-
- $submit.removeAttr('disabled');
- setTimeout(function() {
- $check.hide();
- }, 1000);
- }, function(xhr) {
- $submit.removeAttr('disabled');
- errors.tooltipHandler($submit)(xhr);
+ events: {
+ 'submit .component-config': function(event) {
+ event.preventDefault();
+ var $form = $(event.currentTarget);
+
+ var $submit = $form.find('[type="submit"]');
+ $submit.attr('disabled', 'disabled');
+
+ var formData = $form.serializeObject();
+ // merge form data with component attributes
+ this.component.save(formData, {
+ success: function() {
+ // visual feedback with a check icon
+ var $check = $form.find('.icon-ok');
+ $check.show();
+
+ $submit.removeAttr('disabled');
+ setTimeout(function() {
+ $check.hide();
+ }, 1000);
+ },
+
+ error: function() {
+ var args = Array.prototype.slice.call(arguments, 0);
+ $submit.removeAttr('disabled');
+ errors.tooltipHandler($submit).apply(errors, args);
+ },
+
+ wait: true
});
- },
+ },
- 'li .btn-success click': function($btn, event) {
- event.preventDefault();
- var $element = $btn.siblings('.element');
- $element.trigger('addRequested');
+ 'click li .btn-success': function(event) {
+ event.preventDefault();
+ var $btn = $(event.currentTarget);
+
+ var $element = $btn.siblings('.element');
+ this.publish('elementList:addElement', $element);
+ }
}
});
});
View
365 public/javascripts/views/element.js
@@ -1,35 +1,47 @@
-define(['can', './extended', 'models/element', 'helpers/screen-utils', 'helpers/errors',
- 'can.super', 'lib/bootstrap', 'jquery.ui'],
- function(can, ExtendedControl, ElementModel, screenUtils, errors) {
- return ExtendedControl({
- usedIds: {}
- }, {
- init: function($element, options) {
- this._super($element, options);
- this.elementModel = options.elementModel;
-
- this.componentRow = options.component.attr('row');
- this.componentId = options.component.attr('id');
+define(['jquery', 'backbone', 'underscore', './extended', 'helpers/screen-utils',
+ 'helpers/errors', 'helpers/shared-models', 'lib/bootstrap', 'jquery.ui',
+ 'jquery.serialize'],
+ function($, Backbone, _, ExtendedView, screenUtils, errors, sharedModels) {
+ return ExtendedView.extend({
+ tagName: 'div',
+
+ initialize: function(options) {
+ this.component = options.component;
+ this.componentRow = this.component.get('row');
+ this.componentId = this.component.get('id');
+
+ var type = this.model.get('type');
+ var templateId = type + '-element-template';
+
+ var $template = $('#' + templateId);
+ this.template = _.template($template.html());
+
+ // popovers only on prototype page
+ if (!screenUtils.isSharePage()) {
+ var popoverTemplateId = type + '-popover-template';
+ this.popoverTemplate = _.template($('#' + popoverTemplateId).html());
+ }
- if (options.component.attr('type') === 'form') {
- this.addIdToFormElement();
+ // if a certain tag is specified by the template, use it; otherwise,
+ // default to div
+ var tag = $template.data('tag');
+ if (tag) {
+ this.setElement(this.make(tag));
}
- this.appendAndSetElement();
- this.addPopover();
- this.element.data('model', this.elementModel);
+ // construct after tag is finalized
+ this.constructParent(arguments);
- // for event handlers
- this.off();
- this.options.content = $('#content');
- this.on();
- },
+ if (screenUtils.isSharePage()) {
+ this.$el.addClass('share-element');
+ } else {
+ this.$el.addClass('live-element');
+ }
- appendAndSetElement: function() {
- var type = this.elementModel.attr('type');
- this.options.container.append(can.view(type + '-element-template',
- this.elementModel));
- this.setElement(this.$('.live-element').last());
+ if (this.component.get('type') === 'form') {
+ this.addIdToFormElement();
+ this.$el.addClass('field');
+ }
},
addPopover: function() {
@@ -38,76 +50,78 @@ define(['can', './extended', 'models/element', 'helpers/screen-utils', 'helpers/
return;
}
- var templateId = this.elementModel.attr('type') + '-popover-template';
- var placement = 'bottom';
-
- // TODO: change this when layout additions kick in
- if (this.componentRow === 3) {
- placement = 'top';
- }
+ var self = this;
+ sharedModels.getCurrentScreen()
+ .then(function(screen) {
+ var placement = 'bottom';
+ if (self.componentRow === screen.get('layout').length) {
+ placement = 'top';
+ }
- this.element.popover({
- title: 'Edit Element',
- trigger: 'manual',
- placement: placement,
- content: can.view.render(templateId, this.elementModel)
- });
+ self.$el.popover({
+ title: 'Edit Element',
+ trigger: 'manual',
+ placement: placement,
+ content: self.popoverTemplate(self.getTemplateData())
+ });
+ });
},
render: function() {
- var type = this.elementModel.attr('type');
- var $oldElement = this.element;
-
this.deactivate();
- this.setElement(this.element.parent());
+ this.$el.html(this.template(this.getTemplateData()));
- // replace the current element with a newly rendered one
- var $newElement = $(can.view.render(type + '-element-template', this.elementModel));
- $newElement = $newElement.filter('.live-element');
- $oldElement.replaceWith($newElement);
-
- this.setElement($newElement);
+ // get rid of the current popover entirely if one exists
+ this.$el.data('popover', null);
this.addPopover();
- this.element.data('model', this.elementModel);
+
+ this.$el.data('model', this.model);
+ return this;
+ },
+
+ getTemplateData: function() {
+ var data = this.model.toJSON();
+ data.elementId = this.elementId;
+ return data;
},
activate: function() {
- if (!this.element.hasClass('active')) {
- this.element.popover('show');
- this.element.addClass('active');
+ if (!this.$el.hasClass('active')) {
+ this.$el.popover('show');
+ this.$el.addClass('active');
// add text to selection div inside popover select box
- var $popover = this.element.data('popover').tip();
+ var $popover = this.$el.data('popover').tip();
var $select = $popover.find('.field select');
$select.siblings('.selection')
.text($select.find('option:selected').text());
// if the element model was just created, the user likely wants to
// edit this element; in turn, immediately focus the first input/textarea
- if (this.elementModel.justCreated) {
+ if (this.model.justCreated) {
$popover.find('input, textarea')
.eq(0)
.focus()
.select();
// only do this once
- this.elementModel.justCreated = false;
+ this.model.justCreated = false;
}
}
},
deactivate: function() {
- if (this.element.hasClass('active')) {
- var $popover = this.element.data('popover').tip();
+ if (this.$el.hasClass('active')) {
+ var $popover = this.$el.data('popover').tip();
$popover.find('.btn-primary').tooltip('hide');
- this.element.popover('hide');
- this.element.removeClass('active');
+ this.$el.popover('hide');
+ this.$el.removeClass('active');
}
},
getPreviousElement: function() {
- var $prev = this.element.prev();
+ var $prev = this.$el.prev();
if ($prev.length > 0) {
return $prev.data('model');
@@ -116,7 +130,7 @@ define(['can', './extended', 'models/element', 'helpers/screen-utils', 'helpers/
},
getNextElement: function() {
- var $next = this.element.next();
+ var $next = this.$el.next();
// skip over placeholder added by jqueryui sortable
if ($next.hasClass('ui-sortable-placeholder')) {
@@ -129,84 +143,79 @@ define(['can', './extended', 'models/element', 'helpers/screen-utils', 'helpers/
return null;
},
- // given the current state of this element in the DOM, update the element
- // model linked list to match
- updateElementPosition: function() {
- var elementModel = this.elementModel;
+ // insert this element into the linked list based off of its current
+ // position in the DOM
+ insertElement: function() {
+ var model = this.model;
var previousElement = this.getPreviousElement();
if (previousElement) {
- elementModel.attr('nextId', previousElement.attr('nextId'));
-
- previousElement.attr('nextId', elementModel.attr('id'));
- previousElement.withRouteData({ componentId: this.componentId })
- .save();
+ model.set('nextId', previousElement.get('nextId'));
+ previousElement.save({ nextId: model.get('id') }, { wait: true });
} else {
// no previous element means this is the head
- elementModel.attr('head', true);
+ model.set('head', true);
var nextElement = this.getNextElement();
if (nextElement) {
- elementModel.attr('nextId', nextElement.attr('id'));
- nextElement.attr('head', null);
- nextElement.withRouteData({ componentId: this.componentId })
- .save();
+ model.set('nextId', nextElement.get('id'));
+ nextElement.save({ head: null }, { wait: true });
}
}
- elementModel.withRouteData({ componentId: this.componentId })
- .save();
+ model.save({}, { wait: true });
},
removeElementFromLinkedList: function() {
- var elementModel = this.elementModel;
+ var model = this.model;
var previousElement = this.getPreviousElement();
if (previousElement) {
- var nextId = elementModel.attr('nextId');
+ var nextId = model.get('nextId');
if (nextId) {
// set the previous' nextId to this element's nextId
- previousElement.attr('nextId', elementModel.attr('nextId')) ;
+ previousElement.set('nextId', model.get('nextId'));
} else {
// there is no next; remove the previous' nextId
- previousElement.attr('nextId', null);
+ previousElement.set('nextId', null);
}
- previousElement.withRouteData({ componentId: this.componentId })
- .save();
+ previousElement.save({}, { wait: true });
} else {
var nextElement = this.getNextElement();
if (nextElement) {
// no previous element means nextElement is the new head
- nextElement.attr('head', true);
- nextElement.withRouteData({ componentId: this.componentId })
- .save();
+ nextElement.save({ head: true }, { wait: true });
}
}
// unset the element's nextId and head, but do not save; the element
// will either be deleted promptly (see destroyElement below) or saved
- // later (see updateElementPosition)
- elementModel.attr('nextId', null);
- elementModel.attr('head', null);
+ // later (see insertElement)
+ model.set('nextId', null);
+ model.set('head', null);
},
destroyElement: function() {
var self = this;
- self.removeElementFromLinkedList(true);
- self.elementModel.withRouteData({ componentId: self.componentId })
- .destroy()
- .then(function() {
+ self.removeElementFromLinkedList();
+
+ self.model.destroy({
+ success: function() {
self.deactivate();
- // will also destroy this controller
- self.element.remove();
- });
+ self.undelegateEvents();
+
+ self.off();
+ self.remove();
+ },
+ wait: true
+ });
},
addIdToFormElement: function() {
- var id = this.elementModel.attr('name');
+ var id = this.model.get('name');
var usedIds = this.constructor.usedIds;
// restrict to alphanumeric characters and underscores
@@ -236,92 +245,134 @@ define(['can', './extended', 'models/element', 'helpers/screen-utils', 'helpers/
usedIds[id] = 1;
}
- this.elementModel.attr('elementId', id);
+ this.elementId = id;
},
- '{content} click': function($element, event) {
- this.deactivate();
- },
+ contextualEvents: {
+ 'click window | #content': function(event) {
+ this.deactivate();
+ },
- '{content} .live-element click': function($element, event) {
- if ($element.is(this.element)) {
- event.preventDefault();
+ 'click #content | .live-element': function(event) {
+ var $element = $(event.currentTarget);
+ if ($element.is(this.$el)) {
+ event.preventDefault();
- if (this.element.hasClass('active')) {
- // if already activated, deactivate
- this.deactivate();
+ if (this.$el.hasClass('active')) {
+ // if already activated, deactivate
+ this.deactivate();
+ } else {
+ this.activate();
+ }
} else {
- this.activate();
+ this.deactivate();
}
- } else {
+ },
+
+ 'click window | .close-popover': function(event) {
+ event.preventDefault();
this.deactivate();
- }
- },
+ },
- '{content} deactivateElementRequested': function($element, event) {
- this.deactivate();
- },
+ 'submit window | .popover-content form': function(event) {
+ if (this.$el.hasClass('active')) {
+ event.preventDefault();
+ var self = this;
- '{window} .close-popover click': function($element, event) {
- event.preventDefault();
- this.deactivate();
- },
+ var $form = $(event.currentTarget);
+ var formData = $form.serializeObject();
- '{window} .popover-content form submit': function($form, event) {
- if (this.element.hasClass('active')) {
- event.preventDefault();
- var self = this;
+ for (var attr in formData) {
+ var oldValue = self.model.get(attr);
- var formData = $form.serializeObject();
- for (var attr in formData) {
- var oldValue = self.elementModel.attr(attr);
+ // make the new value's type match the old value's type
+ if (typeof oldValue === 'number' && typeof formData[attr] === 'string') {
+ formData[attr] = +formData[attr];
+ }
- // make the new value's type match the old value's type
- if (typeof oldValue === 'number' && typeof formData[attr] === 'string') {
- formData[attr] = +formData[attr];
+ self.model.set(attr, formData[attr]);
}
- self.elementModel.attr(attr, formData[attr]);
+ self.model.save({}, {
+ success: function() {
+ self.render();
+ },
+
+ error: errors.tooltipHandler($form.find('.btn-primary')),
+ wait: true
+ });
}
+ },
- self.elementModel.withRouteData({ componentId: self.componentId })
- .save()
- .then(function() {
- self.render();
- }, errors.tooltipHandler($form.find('.btn-primary')));
- }
- },
+ 'keydown window | .popover-content textarea': function(event) {
+ if (this.$el.hasClass('active')) {
+ if ((event.ctrlKey || event.metaKey) && event.which === 13) {
+ event.preventDefault();
- '{window} .popover-content textarea keydown': function($textarea, event) {
- if (this.element.hasClass('active')) {
- if ((event.ctrlKey || event.metaKey) && event.which === 13) {
- event.preventDefault();
+ // submit the element edit form
+ $(event.currentTarget).parent().submit();
+ }
+ }
+ },
- // submit the element edit form
- $textarea.parent().submit();
+ 'click window | .popover-content .btn-danger': function(event) {
+ if (this.$el.hasClass('active')) {
+ event.preventDefault();
+ this.deactivate();
+ this.destroyElement();
}
}
},
- '{window} .popover-content .btn-danger click': function($btn, event) {
- if (this.element.hasClass('active')) {
- this.destroyElement();
- }
- },
+ subscriptions: {
+ 'component:activateElement': function($element) {
+ if ($element.is(this.$el)) {
+ this.activate();
+ } else {
+ this.deactivate();
+ }
+ },
+
+ 'component:deactivateElements': function() {
+ this.deactivate();
+ },
- '{window} isolatedKeyDown': function($window, event, keyEvent) {
- if (this.element && this.element.hasClass('active')) {
- // escape key
- if (keyEvent.which === 27) {
+ 'screenActions:deactivateElementsInComponent': function(component) {
+ if (this.component === component) {
this.deactivate();
}
+ },
- // backspace key
- if (keyEvent.which === 8) {
- keyEvent.preventDefault();
- this.destroyElement();
+ 'component:removeElement': function($element) {
+ if ($element.is(this.$el)) {
+ this.removeElementFromLinkedList();
+ }
+ },
+
+ 'component:insertElement': function($element) {
+ if ($element.is(this.$el)) {
+ this.insertElement();
+ }
+ },
+
+ 'keyManager:keyDown': function(keyEvent) {
+ if (this.$el && this.$el.hasClass('active')) {
+ // escape key
+ if (keyEvent.which === 27) {
+ this.deactivate();
+ }
+
+ // backspace key
+ if (keyEvent.which === 8) {
+ keyEvent.preventDefault();
+ this.deactivate();
+ this.destroyElement();
+ }
}
}
}
+ }, {
+ // to keep track of form IDs that are used
+ usedIds: {}
});
});
View
72 public/javascripts/views/extended.js
@@ -3,15 +3,7 @@ define(['jquery', 'backbone', 'underscore', 'router', 'helpers/screen-utils'],
var mediator = _.extend({}, Backbone.Events);
var ExtendedView = Backbone.View.extend({
initialize: function(options) {
- if (screenUtils.isSharePage()) {
- // no event handlers on share page
- this.undelegateEvents();
- } else {
- // subscriptions, route events, and contextual events only when prototyping
- this.addSubscriptions();
- this.addRouteEvents();
- this.addContextualEvents();
- }
+ // nothing to do
},
constructParent: function(args) {
@@ -19,29 +11,37 @@ define(['jquery', 'backbone', 'underscore', 'router', 'helpers/screen-utils'],
ExtendedView.prototype.initialize.apply(this, args);
},
- addSubscriptions: function() {
+ setSubscriptions: function(action) {
var self = this;
+
+ // determine whether to subscribe or unsubscribe
+ if (action === 'on') {
+ action = 'subscribe';
+ } else if (action === 'off') {
+ action = 'unsubscribe';
+ }
+
if (_.isObject(self.subscriptions)) {
// subscribe for each event-callback pair in the subscriptions object
_.each(self.subscriptions, function(callback, eventType) {
callback = self.resolveCallback(callback);
- self.subscribe(eventType, callback, self);
+ self[action](eventType, callback, self);
});
}
},
- addRouteEvents: function() {
+ setRouteEvents: function(action) {
var self = this;
if (_.isObject(self.routeEvents)) {
// add route events for each event-callback pair
_.each(self.routeEvents, function(callback, eventType) {
callback = self.resolveCallback(callback);
- router.on('route:' + eventType, _.bind(callback, self));
+ router[action]('route:' + eventType, _.bind(callback, self));
});
}
},
- addContextualEvents: function() {
+ setContextualEvents: function(action) {
var self = this;
if (_.isObject(self.contextualEvents)) {
// add contextual events for each event-callback pair
@@ -79,7 +79,7 @@ define(['jquery', 'backbone', 'underscore', 'router', 'helpers/screen-utils'],
callback = self.resolveCallback(callback);
// add the event with delegation
- contextElement.on(eventType, selector, _.bind(callback, self));
+ contextElement[action](eventType, selector, _.bind(callback, self));
});
}
},
@@ -94,18 +94,48 @@ define(['jquery', 'backbone', 'underscore', 'router', 'helpers/screen-utils'],
},
publish: function() {
- var arguments = Array.prototype.slice.call(arguments, 0);
- mediator.trigger.apply(mediator, arguments);
+ var args = Array.prototype.slice.call(arguments, 0);
+ mediator.trigger.apply(mediator, args);
},
subscribe: function() {
- var arguments = Array.prototype.slice.call(arguments, 0);
- mediator.on.apply(mediator, arguments);
+ var args = Array.prototype.slice.call(arguments, 0);
+ mediator.on.apply(mediator, args);
+ },
+
+ unsubscribe: function() {
+ var args = Array.prototype.slice.call(arguments, 0);
+ mediator.off.apply(mediator, args);
},
navigate: function() {
- var arguments = Array.prototype.slice.call(arguments, 0);
- router.navigate.apply(router, arguments);
+ var args = Array.prototype.slice.call(arguments, 0);
+ router.navigate.apply(router, args);
+ },
+
+ delegateEvents: function() {
+ // don't delegate events on share page
+ if (!this.eventsDelegated && !screenUtils.isSharePage()) {
+ // also add subscriptions, route events, and contextual events
+ this.setSubscriptions('on');
+ this.setRouteEvents('on');
+ this.setContextualEvents('on');
+
+ Backbone.View.prototype.delegateEvents.call(this);
+ this.eventsDelegated = true;
+ }
+ },
+
+ undelegateEvents: function() {
+ if (this.eventsDelegated) {
+ // also remove subscriptions, route events, and contextual events
+ this.setSubscriptions('off');
+ this.setRouteEvents('off');
+ this.setContextualEvents('off');
+
+ Backbone.View.prototype.undelegateEvents.call(this);
+ this.eventsDelegated = false;
+ }
}
});
View
13 public/javascripts/views/key-manager.js
@@ -0,0 +1,13 @@
+define(['jquery', 'backbone', 'underscore', './extended'],
+ function($, Backbone, _, ExtendedView) {
+ return ExtendedView.extend({
+ events: {
+ 'keydown': function(event) {
+ if ($('input:focus, textarea:focus').length === 0) {
+ // key down should only trigger when it is not meant for a focused element
+ this.publish('keyManager:keyDown', event);
+ }
+ }
+ }
+ });
+});
View
184 public/javascripts/views/layout-modification.js
@@ -1,111 +1,105 @@
-define(['jquery', 'can', './extended', 'helpers/shared-models', 'helpers/utils',
+define(['jquery', 'backbone', 'underscore', './extended', 'helpers/shared-models',
'lib/bootstrap.min'],
- function($, can, ExtendedControl, sharedModels, utils) {
- return ExtendedControl({
- init: function($element, options) {
- this._super($element, options);
- var self = this;
-
- sharedModels.getCurrentScreen()
- .then(function(screen) {
- self.screen = screen;
- self.render();
-
- // change could be called in rapid succession if, say, a splice
- // call both removes and adds an element; because this can cause
- // issues, debounce the callback
- screen.layout.bind('change', utils.debounce(function() {
- self.render();
- }, self, 50));
- }, function() {
- // TODO: handle error
- });
+ function($, Backbone, _, ExtendedView, sharedModels) {
+ return ExtendedView.extend({
+ tagName: 'div',
+ id: 'layout-modifications',
+
+ className: 'clearfix',
+ template: _.template($('#screen-layout-template').html()),
- // for triggering events
- this.off();
- this.options.content = $('#content');
- this.on();
+ initialize: function(options) {
+ this.constructParent(arguments);
+ this.screen = options.screen;
+
+ this.screen.on('change:layout', function() {
+ this.render();
+ }, this);
},
render: function() {
+ this.$el.html(this.template(this.screen.toJSON()));
// activate dropdown
this.$('.dropdown-toggle').dropdown();
+ return this;
},
- saveScreen: function() {
- this.screen.withRouteData()
- .save()
- .then(function(screen) {
- }, function(xhr) {
- // TODO: handle error
+ events: {
+ // to add a row
+ 'click .layout-actions a': function(event) {
+ event.preventDefault();
+ var layout = this.screen.get('layout');
+
+ layout = layout.slice(0); // duplicate array so save() fires change event
+ layout.push([ 4, 4, 4 ]);
+ this.screen.save({ 'layout': layout }, { wait: true });
+ },
+
+ // to modify a row
+ 'click .dropdown-menu .layout-row': function(event) {
+ event.preventDefault();
+ var $row = $(event.currentTarget);
+ var layout = this.screen.get('layout');
+ layout = layout.slice(0); // duplicate array so save() fires change event
+
+ // parent .layout-row contains a data-row attribute with row index
+ var rowIndex = $row.parent()
+ .closest('.layout-row')
+ .data('row');
+ rowIndex = parseInt(rowIndex, 10);
+
+ // get the new column lengths for this row based off of the child elements
+ var newRow = [];
+ $row.children().each(function() {
+ var $col = $(this);
+ var length = $col.data('length');
+
+ length = parseInt(length, 10);
+ newRow.push(length);
});
- },
- // to add a row
- '.layout-actions a click': function($element, event) {
- event.preventDefault();
- var layout = this.screen.attr('layout');
+ var oldRow = layout[rowIndex];
+ // if the number of columns has been reduced, delete all excess components
+ for (var colIndex = newRow.length; colIndex < oldRow.length; colIndex++) {
+ this.publish('layoutModification:deleteComponent', {
+ row: rowIndex,
+ col: colIndex
+ });
+ }
+
+ layout[rowIndex] = newRow;
+ this.screen.save({ 'layout': layout }, { wait: true });
+ },
+
+ // to delete a row
+ 'click .delete-row': function(event) {
+ event.preventDefault();
+ var $link = $(event.currentTarget);
+ var layout = this.screen.get('layout');
+ layout = layout.slice(0); // duplicate array so save() fires change event
+
+ // parent .layout-row contains a data-row attribute with row index
+ var rowIndex = $link.closest('.layout-row')
+ .data('row');
+
+ rowIndex = parseInt(rowIndex, 10);
+ this.publish('layoutModification:deleteRow', rowIndex);
- layout.push([ 4, 4, 4 ]);
- this.saveScreen();
- },
-
- // to modify a row
- '.dropdown-menu .layout-row click': function($row, event) {
- event.preventDefault();
- var layout = this.screen.attr('layout');
-
- // parent .layout-row contains a data-row attribute with row index
- var rowIndex = $row.parent()
- .closest('.layout-row')
- .data('row');
- rowIndex = parseInt(rowIndex, 10);
-
- // get the new column lengths for this row based off of the child elements
- var newRow = [];
- $row.children().each(function() {
- var $col = $(this);
- var length = $col.data('length');
-
- length = parseInt(length, 10);
- newRow.push(length);
- });
-
- var oldRow = layout[rowIndex];
- // if the number of columns has been reduced, delete all excess components
- for (var colIndex = newRow.length; colIndex < oldRow.length; colIndex++) {
- this.options.content.trigger('deleteComponentRequested', {
- row: rowIndex,
- col: colIndex
- });
- }
-
- // use splice instead of layout[row] = newRow so that live binding kicks in
- layout.splice(rowIndex, 1, newRow);
- this.saveScreen();
- },
-
- // to delete a row
- '.delete-row click': function($link, event) {
- event.preventDefault();
- var layout = this.screen.attr('layout');
-
- // parent .layout-row contains a data-row attribute with row index
- var rowIndex = $link.closest('.layout-row')
- .data('row');
-
- rowIndex = parseInt(rowIndex, 10);
- this.options.content.trigger('deleteRowRequested', rowIndex);
-
- var self = this;
- // wait slightly to update screen so that components are fully deleted/
- // updated; this way, the screen layout will be refreshed after components
- // are ready to be placed; although this may not work in all cases, it
- // is a much simpler solution than other options and works in most cases
- setTimeout(function() {
layout.splice(rowIndex, 1);
- self.saveScreen();
- }, 100);
+ this.screen.save({ 'layout': layout }, { wait: true });
+
+ /* TODO: is this wait necessary?
+ var self = this;
+ // wait slightly to update screen so that components are fully deleted/
+ // updated; this way, the screen layout will be refreshed after components
+ // are ready to be placed; although this may not work in all cases, it
+ // is a simple solution and functions in most situations
+ setTimeout(function() {
+ layout.splice(rowIndex, 1);
+ self.saveScreen();
+ }, 100);
+ */
+ }
}
});
});
View
81 public/javascripts/views/screen-actions.js
@@ -1,62 +1,73 @@
-define(['jquery', 'can', './extended', './component-list', './element-list', 'can.super'],
- function($, can, ExtendedControl, ComponentListControl, ElementListControl) {
- return ExtendedControl({
- init: function($element, options) {
- this._super($element, options);
+define(['jquery', 'backbone', 'underscore', './extended', './component-list',
+ './element-list'],
+ function($, Backbone, _, ExtendedView, ComponentListView, ElementListView) {
+ return ExtendedView.extend({
+ initialize: function(options) {
+ this.constructParent(arguments);
- // used for event handlers
- this.off();
- this.options.content = $('#content');
- this.options.back = $('#back');
- this.on();
+ // for caching purposes
+ this.$back = $('#back');
// by default, the component list should be shown
this.activateComponentList();
},
activateComponentList: function() {
- if (this.elementListControl) {
- this.elementListControl.deactivate();
+ if (this.elementListView) {
+ this.elementListView.unrender();
+ this.elementListView.undelegateEvents();
}
- this.options.back.find('span').text('project page');
- if (this.componentListControl) {
- this.componentListControl.activate();
+ this.$back.find('span').text('project page');
+ if (this.componentListView) {
+ this.elementListView.unrender();
+ this.componentListView.delegateEvents();
+ this.componentListView.render();
} else {
- this.componentListControl = new ComponentListControl(this.element, {});
+ this.componentListView = new ComponentListView({ el: this.el });
}
},
activateElementList: function(component) {
- if (this.componentListControl) {
- this.componentListControl.deactivate();
+ if (this.componentListView) {
+ this.componentListView.unrender();
+ this.componentListView.undelegateEvents();
}
- this.options.back.find('span').text('component list');
- if (this.elementListControl) {
- this.elementListControl.activate();
- this.elementListControl.setComponentModel(component);
+ this.$back.find('span').text('component list');
+
+ if (this.elementListView) {
+ this.elementListView.unrender();
+ this.elementListView.setComponentModel(component);
+
+ this.elementListView.delegateEvents();
+ this.elementListView.render();
} else {
- this.elementListControl = new ElementListControl(this.element, { component: component });
+ this.elementListView = new ElementListView({ el: this.el,
+ component: component });
}
},
- '{content} .component-location selected': function($element, event, component) {
- this.activateElementList(component);
+ contextualEvents: {
+ 'click #sidebar | #back': function(event) {
+ if (this.elementListView && this.elementListView.isActive()) {
+ event.preventDefault();
+ // back button should go to component list
+ this.activateComponentList();
+ } else {
+ // back button should link to project page, as it normally does; no
+ // need to do anything
+ }
+ }
},
- '{content} deselectedAll': function($element, event, component) {
- this.activateComponentList();
- },
+ subscriptions: {
+ 'component:selected': function(component) {
+ this.activateElementList(component);
+ },
- '{back} click': function($element, event) {
- if (this.elementListControl && this.elementListControl.isActive()) {
- event.preventDefault();
- // back button should go to component list
+ 'component:deselectedAll': function(event, component) {
this.activateComponentList();
- } else {
- // back button should link to project page, as it normally does; no
- // need to do anything
}
}
});
View
169 public/javascripts/views/screen-layout.js
@@ -1,38 +1,45 @@
-define(['can', './extended', './component', 'models/component', 'helpers/screen-utils',
- 'helpers/errors', 'helpers/shared-models', 'helpers/utils', 'can.super',
- 'jquery.ui'],
- function(can, ExtendedControl, ComponentControl, ComponentModel, screenUtils, errors,
- sharedModels, utils) {
- return ExtendedControl({
- dropOptions: {
- hoverClass: 'component-hover',
- accept: '.component'
- },
-
- init: function($element, options) {
- this._super($element, options);
+define(['jquery', 'backbone', 'underscore', './extended', 'collections/component',
+ './component', 'helpers/screen-utils', 'helpers/errors',
+ 'helpers/shared-models', 'jquery.ui'],
+ function($, Backbone, _, ExtendedView, ComponentCollection, ComponentView,
+ screenUtils, errors, sharedModels) {
+ return ExtendedView.extend({
+ template: _.template($('#layout-template').html()),
+
+ initialize: function(options) {
var self = this;
+ self.constructParent(arguments);
+
+ self.Components = new ComponentCollection([], screenUtils.getUrlData());
+ self.Components.on('add', self.addComponent, self);
+ self.Components.on('reset', self.addAllComponents, self);
+
+ // remove is handled in component.js; no need to deal with it here
+ // self.Components.on('remove', self.addAllComponents, self);
sharedModels.getCurrentScreen()
.then(function(screen) {
self.screen = screen;
self.render();
-
- // change could be called in rapid succession if, say, a splice
- // call both removes and adds an element; because this can cause
- // issues, debounce the callback
- screen.layout.bind('change', utils.debounce(function() {
+ self.Components.fetch();