Skip to content
Browse files

Merge branch 'master' of github.com:mozilla/napkin

  • Loading branch information...
2 parents 70cdc3b + 515c3b6 commit 3b0b0d7fdc8bd743bec138736d367280358137e6 Jen Fong-Adwent committed
Showing with 2,171 additions and 5,110 deletions.
  1. +18 −4 export/index.js
  2. BIN public/images/220x220.gif
  3. +11 −0 public/javascripts/collections/component.js
  4. +12 −0 public/javascripts/collections/element.js
  5. +15 −0 public/javascripts/collections/extended.js
  6. +11 −0 public/javascripts/collections/project.js
  7. +13 −0 public/javascripts/collections/screen.js
  8. +0 −64 public/javascripts/controllers/component-list.js
  9. +0 −318 public/javascripts/controllers/component.js
  10. +0 −93 public/javascripts/controllers/element-list.js
  11. +0 −327 public/javascripts/controllers/element.js
  12. +0 −38 public/javascripts/controllers/extended.js
  13. +0 −111 public/javascripts/controllers/layout-modification.js
  14. +0 −35 public/javascripts/controllers/project-list.js
  15. +0 −127 public/javascripts/controllers/project.js
  16. +0 −63 public/javascripts/controllers/screen-actions.js
  17. +0 −161 public/javascripts/controllers/screen-layout.js
  18. +0 −95 public/javascripts/controllers/screen-list.js
  19. +0 −54 public/javascripts/controllers/screen.js
  20. +0 −23 public/javascripts/controllers/switch.js
  21. +0 −11 public/javascripts/controllers/window.js
  22. +1 −1 public/javascripts/helpers/errors.js
  23. +30 −10 public/javascripts/helpers/shared-models.js
  24. +0 −40 public/javascripts/helpers/utils.js
  25. +39 −0 public/javascripts/lib/backbone.min.js
  26. +0 −42 public/javascripts/lib/can.construct.super.js
  27. +0 −2,994 public/javascripts/lib/can.jquery.js
  28. +0 −1 public/javascripts/lib/can.jquery.min.js
  29. +0 −181 public/javascripts/lib/can.observe.list.sort.js
  30. +32 −0 public/javascripts/lib/underscore.min.js
  31. +2 −8 public/javascripts/models/component.js
  32. +2 −8 public/javascripts/models/element.js
  33. +15 −38 public/javascripts/models/extended.js
  34. +2 −15 public/javascripts/models/project.js
  35. +5 −14 public/javascripts/models/screen.js
  36. +12 −0 public/javascripts/routers/project-page.js
  37. +1 −1 public/javascripts/scripts/example.js
  38. +7 −7 public/javascripts/scripts/index.js
  39. +9 −8 public/javascripts/scripts/prototype.js
  40. +76 −0 public/javascripts/views/component-list.js
  41. +312 −0 public/javascripts/views/component.js
  42. +100 −0 public/javascripts/views/element-list.js
  43. +384 −0 public/javascripts/views/element.js
  44. +143 −0 public/javascripts/views/extended.js
  45. +13 −0 public/javascripts/views/key-manager.js
  46. +105 −0 public/javascripts/views/layout-modification.js
  47. +54 −0 public/javascripts/views/project-list.js
  48. +143 −0 public/javascripts/views/project.js
  49. +74 −0 public/javascripts/views/screen-actions.js
  50. +162 −0 public/javascripts/views/screen-layout.js
  51. +87 −0 public/javascripts/views/screen-list.js
  52. +71 −0 public/javascripts/views/screen.js
  53. +0 −3 public/stylesheets/bootstrap-responsive.css
  54. +1 −1 public/stylesheets/bootstrap-responsive.min.css
  55. +21 −4 public/stylesheets/main.styl
  56. +10 −0 todo.txt
  57. +12 −12 views/example.jade
  58. +12 −5 views/helpers/share-login-form.jade
  59. +4 −0 views/layout.jade
  60. +2 −0 views/prototype.jade
  61. +3 −4 views/templates/collections/project.jade
  62. +15 −16 views/templates/collections/screen.jade
  63. +5 −7 views/templates/elements/auth.jade
  64. +3 −5 views/templates/elements/external-link.jade
  65. +14 −29 views/templates/elements/heading.jade
  66. +6 −9 views/templates/elements/input-checkbox.jade
  67. +6 −9 views/templates/elements/input-radio.jade
  68. +6 −9 views/templates/elements/input-text.jade
  69. +2 −5 views/templates/elements/paragraph.jade
  70. +3 −5 views/templates/elements/screen-link.jade
  71. +6 −9 views/templates/elements/textarea.jade
  72. +4 −0 views/templates/helpers/screen-selection.jade
  73. +4 −5 views/templates/layouts/layout.jade
  74. +1 −1 views/templates/lists/article-element.jade
  75. +38 −38 views/templates/lists/component.jade
  76. +3 −3 views/templates/lists/form-element.jade
  77. +2 −11 views/templates/lists/navigation-element.jade
  78. +2 −2 views/templates/popovers/auth.jade
  79. +3 −3 views/templates/popovers/external-link.jade
  80. +4 −4 views/templates/popovers/heading.jade
  81. +2 −2 views/templates/popovers/input-checkbox.jade
  82. +2 −2 views/templates/popovers/input-radio.jade
  83. +2 −2 views/templates/popovers/input-text.jade
  84. +2 −2 views/templates/popovers/paragraph.jade
  85. +3 −3 views/templates/popovers/screen-link.jade
  86. +2 −2 views/templates/popovers/textarea.jade
  87. +5 −6 views/templates/wrappers/form.jade
View
22 export/index.js
@@ -377,12 +377,26 @@ function renderEjsTemplate(template, attributes, screensById) {
template = template.replace(
/\/share\/#\{session.sharedId\}\/project\/#\{projectId\}\/screen\/#\{screenId\}/g, '');
+ /* Replaces a screen id with its corresponding title slug. Returns a function
+ * that should be used as a regular expression replace callback.
+ * Requires: format for the replacement string, where %s will be replaced
+ * with the slug
+ * Returns: regular expression replace callback assuming that the first
+ * capture group is the screen id
+ */
+ function replaceLink(format) {
+ return function(match, screenId) {
+ screenId = parseInt(screenId, 10);
+ return format.replace(/%s/g, screensById[screenId].titleSlug);
+ };
+ }
+
// screen link targets need to be translated to screen routes
template = template.replace(/\/share\/#\{userId\}\/project\/#\{projectId\}\/screen\/(\d+)/g,
- function(match, screenId) {
- screenId = parseInt(screenId, 10);
- return '/' + screensById[screenId].titleSlug;
- });
+ replaceLink('/%s'));
+ template = template.replace(/\/share\/#\{session.sharedId\}\/project\/#\{projectId\}\/screen\/(\d+)/g,
+ replaceLink('/%s'));
+ template = template.replace(/\?redirect=(\d+)/, replaceLink('?redirect=%s'));
// no need for unescaped attributes
template = template.replace(/!=/g, '=');
View
BIN public/images/220x220.gif
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
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
15 public/javascripts/collections/extended.js
@@ -0,0 +1,15 @@
+define(['jquery', 'backbone'],
+ function($, Backbone) {
+ var ExtendedCollection = Backbone.Collection.extend({
+ initialize: function(models, options) {
+ this.options = options;
+ },
+
+ constructParent: function(args) {
+ args = Array.prototype.slice.call(args, 0);
+ ExtendedCollection.prototype.initialize.apply(this, args);
+ }
+ });
+
+ return ExtendedCollection;
+ });
View
11 public/javascripts/collections/project.js
@@ -0,0 +1,11 @@
+define(['jquery', 'backbone', './extended', 'models/project'],
+ function($, Backbone, ExtendedCollection, ProjectModel) {
+ return ExtendedCollection.extend({
+ url: '/projects',
+ model: ProjectModel,
+
+ comparator: function(project) {
+ return project.get('id');
+ }
+ });
+ });
View
13 public/javascripts/collections/screen.js
@@ -0,0 +1,13 @@
+define(['jquery', 'backbone', './extended', 'models/screen'],
+ function($, Backbone, ExtendedCollection, ScreenModel) {
+ return ExtendedCollection.extend({
+ url: function() {
+ return '/projects/' + this.options.projectId + '/screens';
+ },
+
+ model: ScreenModel,
+ comparator: function(screen) {
+ return screen.get('id');
+ }
+ });
+ });
View
64 public/javascripts/controllers/component-list.js
@@ -1,64 +0,0 @@
-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);
- var self = this;
-
- sharedModels.getCurrentScreen()
- .then(function(screen) {
- self.screen = screen;
- self.activate();
- }, function() {
- // TODO: handle error
- });
- },
-
- dragOptions: {
- revert: 'invalid',
- // don't cancel if input is dragged
- cancel: ''
- },
-
- render: function() {
- this.element.html(can.view('component-list-template', this.screen));
- this.$('.component').draggable(this.dragOptions);
-
- // to control screen layout modifications
- new LayoutModificationControl(this.$('#layout-modifications'), {});
- this.$('.dropdown-toggle').dropdown();
- },
-
- '#screen-config submit': function($form, event) {
- event.preventDefault();
- 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;
- }
-
- // 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();
-
- $submit.removeAttr('disabled');
- setTimeout(function() {
- $check.hide();
- }, 1000);
- }, function(xhr) {
- $submit.removeAttr('disabled');
- errors.tooltipHandler($submit)(xhr);
- });
- }
- });
- });
View
318 public/javascripts/controllers/component.js
@@ -1,318 +0,0 @@
-define(['can', './extended', './element', 'models/element', 'helpers/screen-utils',
- 'helpers/errors', 'can.super', 'jquery.ui'],
- function(can, ExtendedControl, ElementControl, ElementModel, screenUtils, errors) {
- return ExtendedControl({
- 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();
-
- var type = this.getType();
- this.element.addClass('has-component');
- this.element.addClass(type + '-container');
-
- this.addAllElements();
- if (!screenUtils.isSharePage()) {
- this.element.sortable(this.sortableOptions);
- }
- },
-
- sortableOptions: {
- 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 {
- // TODO: optimize this when layout row is removed (new request for
- // every moved component)
- 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 originalContainerHTML;
-
- elements.each(function(element, index) {
- idElementMap[element.attr('id')] = element;
-
- // begin with the head
- if (element.attr('head')) {
- curElement = element;
- }
- });
-
- // if there are elements to add
- if (curElement) {
- if (self.element.hasClass('empty')) {
- // component is no longer empty
- self.setComponentEmpty(false);
- }
-
- 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));
-
- // where the elements should be added to
- 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('');
- }
- }
-
- 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')];
- }
-
- if (originalContainerHTML) {
- elementsContainer.append(originalContainerHTML);
- }
- },
-
- addElement: function(element, container) {
- var type = element.attr('type');
- new ElementControl(this.element, {
- elementModel: element,
- container: container,
- component: this.component
- });
- },
-
- 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
- });
- },
-
- select: function() {
- if (!this.element.hasClass('active')) {
- this.element.addClass('active');
- this.element.trigger('selected', this.component);
- }
- },
-
- deselect: function(triggerDeselectedAll) {
- if (this.element.hasClass('active')) {
- this.element.removeClass('active');
- this.element.trigger('deselected', this.component);
-
- if (triggerDeselectedAll) {
- this.options.content.trigger('deselectedAll', this.component);
- }
- }
- },
-
- getLastElement: function() {
- return this.$('.live-element').last()
- .data('model');
- },
-
- '{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.originalPrev = $element.prev();
- this.options.content.trigger('deactivateElementRequested');
- }
- },
-
- '{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();
-
- if (this.originalPrev.is($element.prev())) {
- // element wasn't moved; treat this as a click and activate the popover
- elementControl.activate();
- }
- }
- },
-
- '{content} click': function($element, event) {
- this.deselect(true);
- },
-
- '{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'));
- }
- },
-
- '{$sidebar} .element addRequested': function($element, event) {
- if (this.element.hasClass('active')) {
- var self = this;
- // gather all element data
- var data = $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 element = new ElementModel(data);
- var lastElement = self.getLastElement();
-
- if (!lastElement) {
- element.attr('head', true);
- }
-
- // 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();
- }
- },
-
- '{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
- });
- }
- },
-
- '{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);
- }
-
- // backspace key
- if (keyEvent.which === 8) {
- keyEvent.preventDefault();
- this.remove();
- }
- }
- }
- });
- });
View
93 public/javascripts/controllers/element-list.js
@@ -1,93 +0,0 @@
-define(['jquery', 'can', './switch', 'helpers/errors', 'can.super', 'jquery.serialize'],
- function($, can, SwitchControl, errors) {
- return SwitchControl({
- init: function($element, options) {
- this._super($element, options);
- this.setComponentModel(options.component);
- },
-
- deactivate: function() {
- this.unrender();
- this._super();
- },
-
- setComponentModel: function(component) {
- if (this.component !== component) {
- this.deactivate();
- this.component = component;
- this.activate();
- }
- },
-
- render: function() {
- var type = this.component.attr('type');
- this.element.html(can.view(type + '-element-list-template', this.component));
-
- this.element.addClass('elements');
- this.element.addClass(type + '-elements');
- this.centerAddButtons();
-
- // prevent links/inputs on sidebar from being tabbed to so that user
- // can edit elements in the main content area by hitting tab
- this.$('a, input').attr('tabindex', '-1');
-
- // add text to selection div inside select box
- var $select = this.$('.field select');
- $select.siblings('.selection')
- .text($select.find('option:selected').text());
- },
-
- unrender: function() {
- if (this.component) {
- var type = this.component.attr('type');
- this.element.removeClass('elements');
- this.element.removeClass(type + '-elements');
- }
- },
-
- centerAddButtons: function() {
- this.$('.btn').each(function() {
- var $btn = $(this);
- var $element = $btn.siblings('.element');
-
- if ($element.height() > $btn.height()) {
- // 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);
- });
- },
-
- 'li .btn-success click': function($btn, event) {
- event.preventDefault();
- var $element = $btn.siblings('.element');
- $element.trigger('addRequested');
- }
- });
- });
View
327 public/javascripts/controllers/element.js
@@ -1,327 +0,0 @@
-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');
-
- if (options.component.attr('type') === 'form') {
- this.addIdToFormElement();
- }
-
- this.appendAndSetElement();
- this.addPopover();
- this.element.data('model', this.elementModel);
-
- // for event handlers
- this.off();
- this.options.content = $('#content');
- this.on();
- },
-
- 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());
- },
-
- addPopover: function() {
- if (screenUtils.isSharePage()) {
- // no popovers on the screen share page
- 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';
- }
-
- this.element.popover({
- title: 'Edit Element',
- trigger: 'manual',
- placement: placement,
- content: can.view.render(templateId, this.elementModel)
- });
- },
-
- render: function() {
- var type = this.elementModel.attr('type');
- var $oldElement = this.element;
-
- this.deactivate();
- this.setElement(this.element.parent());
-
- // 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);
- this.addPopover();
- this.element.data('model', this.elementModel);
- },
-
- activate: function() {
- if (!this.element.hasClass('active')) {
- this.element.popover('show');
- this.element.addClass('active');
-
- // add text to selection div inside popover select box
- var $popover = this.element.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) {
- $popover.find('input, textarea')
- .eq(0)
- .focus()
- .select();
-
- // only do this once
- this.elementModel.justCreated = false;
- }
- }
- },
-
- deactivate: function() {
- if (this.element.hasClass('active')) {
- var $popover = this.element.data('popover').tip();
- $popover.find('.btn-primary').tooltip('hide');
-
- this.element.popover('hide');
- this.element.removeClass('active');
- }
- },
-
- getPreviousElement: function() {
- var $prev = this.element.prev();
-
- if ($prev.length > 0) {
- return $prev.data('model');
- }
- return null;
- },
-
- getNextElement: function() {
- var $next = this.element.next();
-
- // skip over placeholder added by jqueryui sortable
- if ($next.hasClass('ui-sortable-placeholder')) {
- $next = $next.next();
- }
-
- if ($next.length > 0) {
- return $next.data('model');
- }
- 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;
- var previousElement = this.getPreviousElement();
-
- if (previousElement) {
- elementModel.attr('nextId', previousElement.attr('nextId'));
-
- previousElement.attr('nextId', elementModel.attr('id'));
- previousElement.withRouteData({ componentId: this.componentId })
- .save();
- } else {
- // no previous element means this is the head
- elementModel.attr('head', true);
- var nextElement = this.getNextElement();
-
- if (nextElement) {
- elementModel.attr('nextId', nextElement.attr('id'));
- nextElement.attr('head', null);
- nextElement.withRouteData({ componentId: this.componentId })
- .save();
- }
- }
-
- elementModel.withRouteData({ componentId: this.componentId })
- .save();
- },
-
- removeElementFromLinkedList: function() {
- var elementModel = this.elementModel;
- var previousElement = this.getPreviousElement();
-
- if (previousElement) {
- var nextId = elementModel.attr('nextId');
-
- if (nextId) {
- // set the previous' nextId to this element's nextId
- previousElement.attr('nextId', elementModel.attr('nextId')) ;
- } else {
- // there is no next; remove the previous' nextId
- previousElement.attr('nextId', null);
- }
-
- previousElement.withRouteData({ componentId: this.componentId })
- .save();
- } 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();
- }
- }
-
- // 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);
- },
-
- destroyElement: function() {
- var self = this;
- self.removeElementFromLinkedList(true);
- self.elementModel.withRouteData({ componentId: self.componentId })
- .destroy()
- .then(function() {
- self.deactivate();
- // will also destroy this controller
- self.element.remove();
- });
- },
-
- addIdToFormElement: function() {
- var id = this.elementModel.attr('name');
- var usedIds = this.constructor.usedIds;
-
- // restrict to alphanumeric characters and underscores
- id = id.replace(/ /g, '_');
- id = id.replace(/[^a-zA-Z0-9_\-]/g, '');
-
- // an ID must start with a letter, dash, or underscore; if it starts with
- // a dash, there are further rules involved, so just prepend an
- // underscore if the generated ID begins with either a dash or number
- if (/[0-9\-]/.test(id[0])) {
- id = '_' + id;
- }
-
- // remove all numeric characters from the end of this id
- id = id.replace(/[0-9]+$/g, '');
-
- // note that usedIds[id] contains a count of how many ids there are with
- // the indexed id as a base; i.e. 'id', 'id1', 'id2', 'id3', etc. all
- // have the same base of 'id'
- if (!usedIds[id]) {
- usedIds[id] = 1;
- } else {
- usedIds[id]++;
-
- // this will append the count to id to create a unique id
- id = id + (usedIds[id] - 1);
- usedIds[id] = 1;
- }
-
- this.elementModel.attr('elementId', id);
- },
-
- '{content} click': function($element, event) {
- this.deactivate();
- },
-
- '{content} .live-element click': function($element, event) {
- if ($element.is(this.element)) {
- event.preventDefault();
-
- if (this.element.hasClass('active')) {
- // if already activated, deactivate
- this.deactivate();
- } else {
- this.activate();
- }
- } else {
- this.deactivate();
- }
- },
-
- '{content} deactivateElementRequested': function($element, event) {
- this.deactivate();
- },
-
- '{window} .close-popover click': function($element, event) {
- event.preventDefault();
- this.deactivate();
- },
-
- '{window} .popover-content form submit': function($form, event) {
- if (this.element.hasClass('active')) {
- event.preventDefault();
- var self = this;
-
- 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];
- }
-
- self.elementModel.attr(attr, formData[attr]);
- }
-
- self.elementModel.withRouteData({ componentId: self.componentId })
- .save()
- .then(function() {
- self.render();
- }, errors.tooltipHandler($form.find('.btn-primary')));
- }
- },
-
- '{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
- $textarea.parent().submit();
- }
- }
- },
-
- '{window} .popover-content .btn-danger click': function($btn, event) {
- if (this.element.hasClass('active')) {
- this.destroyElement();
- }
- },
-
- '{window} isolatedKeyDown': function($window, event, keyEvent) {
- if (this.element && this.element.hasClass('active')) {
- // escape key
- if (keyEvent.which === 27) {
- this.deactivate();
- }
-
- // backspace key
- if (keyEvent.which === 8) {
- keyEvent.preventDefault();
- this.destroyElement();
- }
- }
- }
- });
- });
View
38 public/javascripts/controllers/extended.js
@@ -1,38 +0,0 @@
-define(['can', 'helpers/utils', 'helpers/screen-utils'],
- function(can, utils, screenUtils) {
- return can.Control({
- init: function($element, options) {
- this.$ = utils.bind($element.find, $element);
-
- if (screenUtils.isSharePage()) {
- // no event handlers on share page
- this.off();
- }
- },
-
- setElement: function($element) {
- var curControls = this.element.data('controls');
- var newControls = $element.data('controls');
-
- this.off();
-
- // can uses the element's controls data internally to tell what
- // controllers are attached
- if (newControls) {
- newControls.push(this);
- } else {
- $element.data('controls', [ this ]);
- }
- curControls.splice(can.inArray(this, curControls), 1);
-
- // reset the element and re-bind handlers
- this.element = $element;
- if (!screenUtils.isSharePage()) {
- this.on();
- }
-
- // re-add the shortcut find function
- this.$ = utils.bind($element.find, $element);
- }
- });
-});
View
111 public/javascripts/controllers/layout-modification.js
@@ -1,111 +0,0 @@
-define(['jquery', 'can', './extended', 'helpers/shared-models', 'helpers/utils',
- '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
- });
-
- // for triggering events
- this.off();
- this.options.content = $('#content');
- this.on();
- },
-
- render: function() {
- // activate dropdown
- this.$('.dropdown-toggle').dropdown();
- },
-
- saveScreen: function() {
- this.screen.withRouteData()
- .save()
- .then(function(screen) {
- }, function(xhr) {
- // TODO: handle error
- });
- },
-
- // to add a row
- '.layout-actions a click': function($element, event) {
- event.preventDefault();
- var layout = this.screen.attr('layout');
-
- 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);
- }
- });
- });
View
35 public/javascripts/controllers/project-list.js
@@ -1,35 +0,0 @@
-define(['jquery', 'can', './extended', 'models/project', './project',
- 'helpers/errors', 'can.super'],
- function($, can, ExtendedControl, ProjectModel, ProjectControl, errors) {
- can.route.ready(false);
-
- return ExtendedControl({
- init: function($element, options) {
- this._super($element, options);
- var $projects = $('#projects');
-
- // create a ProjectControl for each project
- ProjectModel.findAll()
- .then(function(projects) {
- can.each(projects, function(project, index) {
- new ProjectControl($projects, { project: project });
- });
-
- can.route.ready(true);
- });
- },
-
- // to add a project
- '#add-project-form submit': function($element, event) {
- event.preventDefault();
- var $input = $('#project-title');
-
- var project = new ProjectModel({ title: $input.val() });
- project.save()
- .then(function(project) {
- new ProjectControl('#projects', { project: project });
- $input.val('');
- }, errors.tooltipHandler($input));
- }
- });
- });
View
127 public/javascripts/controllers/project.js
@@ -1,127 +0,0 @@
-define(['can', './extended', 'helpers/errors', 'can.super'], function(can, ExtendedControl, errors) {
- return ExtendedControl({
- init: function($element, options) {
- this._super($element, options);
- this.viewData = new can.Observe(options);
- this.project = this.viewData.project;
-
- $element.append(can.view('project-template', this.viewData));
- this.setElement($element.children().last());
- this.$('i').tooltip({ placement: 'bottom' });
-
- // for event handlers
- this.off();
- this.options.$sidebar = $('#sidebar');
- this.on();
- },
-
- 'project/:projectId route': function(data) {
- var projectId = parseInt(data.projectId, 10);
- if (this.project.attr('id') === projectId) {
- this.viewData.attr('active', 'active');
- } else {
- this.viewData.removeAttr('active');
- }
- },
-
- // to begin editing a project
- '.icon-pencil click': function($element, event) {
- event.preventDefault();
- this.viewData.attr('editing', 'editing');
-
- var $edit = this.$('.edit');
- $edit.focus();
- $edit.select();
- },
-
- // to edit a project
- '.edit keypress': function($element, event) {
- var self = this;
-
- // enter key pressed
- if (event.which === 13) {
- self.project.attr('title', $element.val());
- self.project.save()
- .then(function(project) {
- self.viewData.removeAttr('editing');
- }, errors.tooltipHandler($element));
- }
- },
-
- // to stop editing a project
- '.icon-remove click': function($element, event) {
- event.preventDefault();
- if (this.viewData.attr('editing')) {
- this.viewData.removeAttr('editing');
- this.$('.edit').val(this.project.attr('title'));
- }
- },
-
- // to export a project
- '.icon-inbox click': function($element, event) {
- event.preventDefault();
- window.location.href = '/export/project/' + this.project.attr('id');
- },
-
- // to delete a project
- '{$sidebar} .icon-trash click': function($element, event) {
- event.preventDefault();
- event.stopPropagation();
-
- var $li = $element.closest('li');
- if ($li.is(this.element)) {
- // if this project's trash icon was clicked, open up a popover
- // confirmation dialog
- var self = this;
-
- if (!this.element.data('popover')) {
- this.element.popover({
- title: 'Delete Project',
- trigger: 'manual',
- placement: 'bottom',
- content: can.view.render('#project-deletion-template', {})
- });
- }
-
- this.element.popover('show');
- this.element.addClass('popover-active');
- } else if (this.element.hasClass('popover-active')) {
- // otherwise, if this project's trash icon was not clicked and it has
- // a popover currently active, hide it
- this.element.popover('hide');
- this.element.removeClass('popover-active');
- }
- },
-
- '{window} .popover-content .btn-danger click': function($element, event) {
- if (this.element.hasClass('popover-active')) {
- var self = this;
-
- // user has confirmed they want to delete this project
- self.project.destroy()
- .then(function(project) {
- self.element.popover('hide');
- self.element.remove();
- }, errors.tooltipHandler($element));
- }
- },
-
- '{window} .close-popover click': function($element, event) {
- if (this.element.hasClass('popover-active')) {
- event.preventDefault();
- this.element.popover('hide');
- }
- },
-
- '{window} click': function($element, event) {
- if (this.element.hasClass('popover-active')) {
- var $target = $(event.target);
-
- // if the click occurred outside the popover hide it
- if ($target.closest('.popover').length === 0) {
- this.element.popover('hide');
- }
- }
- }
- });
-});
View
63 public/javascripts/controllers/screen-actions.js
@@ -1,63 +0,0 @@
-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);
-
- // used for event handlers
- this.off();
- this.options.content = $('#content');
- this.options.back = $('#back');
- this.on();
-
- // by default, the component list should be shown
- this.activateComponentList();
- },
-
- activateComponentList: function() {
- if (this.elementListControl) {
- this.elementListControl.deactivate();
- }
-
- this.options.back.find('span').text('project page');
- if (this.componentListControl) {
- this.componentListControl.activate();
- } else {
- this.componentListControl = new ComponentListControl(this.element, {});
- }
- },
-
- activateElementList: function(component) {
- if (this.componentListControl) {
- this.componentListControl.deactivate();
- }
-
- this.options.back.find('span').text('component list');
- if (this.elementListControl) {
- this.elementListControl.activate();
- this.elementListControl.setComponentModel(component);
- } else {
- this.elementListControl = new ElementListControl(this.element, { component: component });
- }
- },
-
- '{content} .component-location selected': function($element, event, component) {
- this.activateElementList(component);
- },
-
- '{content} deselectedAll': function($element, event, component) {
- this.activateComponentList();
- },
-
- '{back} click': function($element, event) {
- if (this.elementListControl && this.elementListControl.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
- }
- }
- });
- });
View
161 public/javascripts/controllers/screen-layout.js
@@ -1,161 +0,0 @@
-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);
- 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));
- });
- },
-
- render: function() {
- this.element.html(can.view('layout-template', this.screen));
- this.configureDropTargets();
-
- this.markComponentPositions();
- this.addAllComponents();
- },
-
- configureDropTargets: function() {
- if (screenUtils.isSharePage()) {
- // no drop targets on share page
- return;
- }
-
- var self = this;
- this.$('.component-location').each(function() {
- var $this = $(this);
- $this.droppable(self.dropOptions);
- });
- },
-
- markComponentPositions: function() {
- this.$('.row').each(function(rowIndex) {
- var $row = $(this);
-
- $row.find('[class^="span"]').each(function(colIndex) {
- var $col = $(this);
-
- // associate a row # and a col # with each column
- $col.data('position', {
- row: rowIndex,
- col: colIndex
- });
-
- // add in an attribute with the same data for easy access
- $col.attr('data-position', rowIndex + ':' + colIndex);
- });
- });
- },
-
- addAllComponents: function() {
- var self = this;
-
- if (self.cachedComponents) {
- // already have components; add each one
- self.addEachComponent();
- } else {
- ComponentModel.withRouteData()
- .findAll()
- .then(function(components) {
- self.cachedComponents = components;
- self.addEachComponent();
- }, function(xhr) {
- // TODO: handle error
- });
- }
- },
-
- addEachComponent: function() {
- var self = this;
- self.cachedComponents.each(function(component, index) {
- self.addComponent(component);
- });
- },
-
- addComponent: function(component) {
- // select the component via the data-position attribute added earlier
- var positionAttr = component.row + ':' + component.col;
- var $componentLocation = this.$('[data-position="' + positionAttr + '"]');
-
- if ($componentLocation.length === 0) {
- // invalid location; ignore this component
- return;
- }
-
- var componentControl = new ComponentControl($componentLocation,
- { component: component });
- this.setComponentControl($componentLocation, componentControl);
- },
-
- componentControls: {},
- getComponentControl: function($componentLocation) {
- var position = $componentLocation.data('position');
- var key = position.row + ':' + position.col;
- return this.componentControls[key];
- },
-
- setComponentControl: function($componentLocation, componentControl) {
- var self = this;
- var position = $componentLocation.data('position');
- var key = position.row + ':' + position.col;
-
- self.componentControls[key] = componentControl;
- can.bind.call(componentControl, 'destroyed', function() {
- delete self.componentControls[key];
- });
- },
-
- '.component-location drop': function($componentLocation, event, ui) {
- var self = this;
- var $component = $(ui.draggable);
- var componentType = $component.data('type');
- var existingControl = self.getComponentControl($componentLocation);
-
- // reset component position, since it was just dragged
- $component.css({ top: 0, left: 0 });
-
- if (existingControl) {
- // remove the existing component if its type is different from the
- // one being added; otherwise, simply focus the component
- if (existingControl.getType() === componentType) {
- existingControl.select();
- } else {
- existingControl.remove();
- }
- }
-
- // create new component and an associated control
- var component = new ComponentModel(can.extend({ type: componentType },
- $componentLocation.data('position')));
-
- component.withRouteData()
- .save()
- .then(function(component) {
- var componentControl = new ComponentControl($componentLocation,
- { component: component });
- self.setComponentControl($componentLocation, componentControl);
- componentControl.select();
- }, errors.tooltipHandler($component));
- }
- });
- });
View
95 public/javascripts/controllers/screen-list.js
@@ -1,95 +0,0 @@
-define(['can', './extended', 'models/screen', './screen', 'helpers/errors', 'can.super'],
- function(can, ExtendedControl, ScreenModel, ScreenControl, errors) {
- return ExtendedControl({
- init: function($element, options) {
- this._super($element, options);
- this.element.hide();
- },
-
- 'project/:projectId route': function(data) {
- this.element.show();
- this.projectId = data.projectId;
- this.renderAllScreens();
- },
-
- 'route': function(data) {
- // revert to hidden state when empty route is triggered
- this.element.hide();
- this.projectId = null;
- },
-
- numScreens: 0,
- renderAllScreens: function() {
- var self = this;
-
- // create a ScreenControl for each project
- ScreenModel.findAll({ projectId: self.projectId })
- .then(function(screens) {
- self.cachedScreens = screens;
- self.clearScreens();
- self.renderEachScreen();
- self.bindListChangeEvents();
- });
- },
-
- // when the screen list changes, automatically re-render display
- bindListChangeEvents: function() {
- var self = this;
- var screens = self.cachedScreens;
-
- screens.bind('change', function(event, what, how, data) {
- if (how === 'remove') {
- self.clearScreens();
- self.renderEachScreen();
- }
- });
- },
-
- clearScreens: function() {
- this.numScreens = 0;
- this.element.children('.screen-row').remove();
- },
-
- renderEachScreen: function() {
- var self = this;
- self.cachedScreens.each(function(screen) {
- self.renderScreen(screen);
- });
- },
-
- renderScreen: function(screen) {
- var self = this;
- var $screenRow;
-
- // insert a new row for every four screens
- if (self.numScreens % 4 === 0) {
- self.element.append(can.view('screen-row-template'), {});
- }
-
- $screenRow = self.element.children().last();
- new ScreenControl($screenRow,
- { screen: screen, projectId: self.projectId });
-
- self.numScreens++;
- },
-
- // to add a screen
- '#add-screen form submit': function($element, event) {
- event.preventDefault();
- var self = this;
-
- var $input = $('#screen-title');
- var screen = new ScreenModel({
- projectId: self.projectId,
- title: $input.val()
- });
-
- screen.save()
- .then(function(screen) {
- self.cachedScreens.push(screen);
- self.renderScreen(screen);
- $input.val('');
- }, errors.tooltipHandler($input));
- }
- });
- });
View
54 public/javascripts/controllers/screen.js
@@ -1,54 +0,0 @@
-define(['can', './extended', 'helpers/errors', 'can.super'], function(can, ExtendedControl, errors) {
- return ExtendedControl({
- init: function($element, options) {
- this._super($element, options);
- this.viewData = new can.Observe(options);
- this.screen = this.viewData.screen;
- this.screen.attr('projectId', this.options.projectId);
-
- $element.append(can.view('screen-template', this.viewData));
- this.setElement($element.find('.screen').last());
- },
-
- // to begin editing a project
- '.icon-pencil click': function($element, event) {
- event.preventDefault();
- this.viewData.attr('editing', 'editing');
-
- var $edit = this.$('.edit');
- $edit.focus();
- $edit.select();
- },
-
- // to edit a screen
- '.edit keypress': function($element, event) {
- var self = this;
-
- // enter key pressed
- if (event.which === 13) {
- self.screen.attr('title', $element.val());
- self.screen.save({ projectId: this.projectId })
- .then(function(screen) {
- self.viewData.removeAttr('editing');
- }, errors.tooltipHandler($element));
- }
- },
-
- // to stop editing a screen
- '.icon-remove click': function($element, event) {
- event.preventDefault();
- if (this.viewData.attr('editing')) {
- this.viewData.removeAttr('editing');
- this.$('.edit').val(this.screen.attr('title'));
- }
- },
-
- // to delete a screen
- '.icon-trash click': function($element, event) {
- event.preventDefault();
- this.screen.destroy()
- .then(function(screen) {
- }, errors.tooltipHandler($element));
- }
- });
-});
View
23 public/javascripts/controllers/switch.js
@@ -1,23 +0,0 @@
-define(['can', './extended', 'can.super'], function(can, ExtendedControl) {
- return ExtendedControl({
- activate: function() {
- if (!this.activated) {
- this.activated = true;
- // user-defined render function
- this.render();
- this.on();
- }
- },
-
- deactivate: function() {
- if (this.activated) {
- this.activated = false;
- this.off();
- }
- },
-
- isActive: function() {
- return this.activated;
- }
- });
-});
View
11 public/javascripts/controllers/window.js
@@ -1,11 +0,0 @@
-define(['jquery', 'can', './extended'], function($, can, ExtendedControl) {
- return ExtendedControl({
- 'keydown': function($element, event) {
- if ($('input:focus, textarea:focus').length === 0) {
- // isolatedKeyDown refers to the fact that this keydown is not meant
- // for a focused element on the page; rather, it is an isolated event
- this.element.trigger('isolatedKeyDown', event);
- }
- }
- });
-});
View
2 public/javascripts/helpers/errors.js
@@ -6,7 +6,7 @@ define(['lib/bootstrap.min'], function() {
*/
tooltipHandler: function($element, placement, howLong) {
var self = this;
- return function(xhr) {
+ return function(model, xhr) {
self.displayTooltip($element, xhr.responseText, placement, howLong);
$element.focus();
};
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
39 public/javascripts/lib/backbone.min.js
@@ -0,0 +1,39 @@
+// Backbone.js 0.9.2
+
+// (c) 2010-2012 Jeremy Ashkenas, DocumentCloud Inc.
+// Backbone may be freely distributed under the MIT license.
+// For all details and documentation:
+// http://backbonejs.org
+(function(h,g){typeof exports!=="undefined"?g(h,exports,require("underscore")):typeof define==="function"&&define.amd?define(["underscore","jquery","exports"],function(f,i,p){h.Backbone=g(h,p,f,i)}):h.Backbone=g(h,{},h._,h.jQuery||h.Zepto||h.ender)})(this,function(h,g,f,i){var p=h.Backbone,y=Array.prototype.slice,z=Array.prototype.splice;g.VERSION="0.9.2";g.setDomLibrary=function(a){i=a};g.noConflict=function(){h.Backbone=p;return g};g.emulateHTTP=false;g.emulateJSON=false;var q=/\s+/,l=g.Events=
+{on:function(a,b,c){var d,e,f,g,j;if(!b)return this;a=a.split(q);for(d=this._callbacks||(this._callbacks={});e=a.shift();)f=(j=d[e])?j.tail:{},f.next=g={},f.context=c,f.callback=b,d[e]={tail:g,next:j?j.next:f};return this},off:function(a,b,c){var d,e,k,g,j,h;if(e=this._callbacks){if(!a&&!b&&!c)return delete this._callbacks,this;for(a=a?a.split(q):f.keys(e);d=a.shift();)if(k=e[d],delete e[d],k&&(b||c))for(g=k.tail;(k=k.next)!==g;)if(j=k.callback,h=k.context,b&&j!==b||c&&h!==c)this.on(d,j,h);return this}},
+trigger:function(a){var b,c,d,e,f,g;if(!(d=this._callbacks))return this;f=d.all;a=a.split(q);for(g=y.call(arguments,1);b=a.shift();){if(c=d[b])for(e=c.tail;(c=c.next)!==e;)c.callback.apply(c.context||this,g);if(c=f){e=c.tail;for(b=[b].concat(g);(c=c.next)!==e;)c.callback.apply(c.context||this,b)}}return this}};l.bind=l.on;l.unbind=l.off;var o=g.Model=function(a,b){var c;a||(a={});b&&b.parse&&(a=this.parse(a));if(c=n(this,"defaults"))a=f.extend({},c,a);if(b&&b.collection)this.collection=b.collection;
+this.attributes={};this._escapedAttributes={};this.cid=f.uniqueId("c");this.changed={};this._silent={};this._pending={};this.set(a,{silent:true});this.changed={};this._silent={};this._pending={};this._previousAttributes=f.clone(this.attributes);this.initialize.apply(this,arguments)};f.extend(o.prototype,l,{changed:null,_silent:null,_pending:null,idAttribute:"id",initialize:function(){},toJSON:function(){return f.clone(this.attributes)},get:function(a){return this.attributes[a]},escape:function(a){var b;
+if(b=this._escapedAttributes[a])return b;b=this.get(a);return this._escapedAttributes[a]=f.escape(b==null?"":""+b)},has:function(a){return this.get(a)!=null},set:function(a,b,c){var d,e;f.isObject(a)||a==null?(d=a,c=b):(d={},d[a]=b);c||(c={});if(!d)return this;if(d instanceof o)d=d.attributes;if(c.unset)for(e in d)d[e]=void 0;if(!this._validate(d,c))return false;if(this.idAttribute in d)this.id=d[this.idAttribute];var b=c.changes={},g=this.attributes,h=this._escapedAttributes,j=this._previousAttributes||
+{};for(e in d){a=d[e];if(!f.isEqual(g[e],a)||c.unset&&f.has(g,e))delete h[e],(c.silent?this._silent:b)[e]=true;c.unset?delete g[e]:g[e]=a;!f.isEqual(j[e],a)||f.has(g,e)!=f.has(j,e)?(this.changed[e]=a,c.silent||(this._pending[e]=true)):(delete this.changed[e],delete this._pending[e])}c.silent||this.change(c);return this},unset:function(a,b){(b||(b={})).unset=true;return this.set(a,null,b)},clear:function(a){(a||(a={})).unset=true;return this.set(f.clone(this.attributes),a)},fetch:function(a){var a=
+a?f.clone(a):{},b=this,c=a.success;a.success=function(d,e,f){if(!b.set(b.parse(d,f),a))return false;c&&c(b,d)};a.error=g.wrapError(a.error,b,a);return(this.sync||g.sync).call(this,"read",this,a)},save:function(a,b,c){var d,e;f.isObject(a)||a==null?(d=a,c=b):(d={},d[a]=b);c=c?f.clone(c):{};if(c.wait){if(!this._validate(d,c))return false;e=f.clone(this.attributes)}a=f.extend({},c,{silent:true});if(d&&!this.set(d,c.wait?a:c))return false;var k=this,h=c.success;c.success=function(a,b,e){b=k.parse(a,e);
+c.wait&&(delete c.wait,b=f.extend(d||{},b));if(!k.set(b,c))return false;h?h(k,a):k.trigger("sync",k,a,c)};c.error=g.wrapError(c.error,k,c);b=this.isNew()?"create":"update";b=(this.sync||g.sync).call(this,b,this,c);c.wait&&this.set(e,a);return b},destroy:function(a){var a=a?f.clone(a):{},b=this,c=a.success,d=function(){b.trigger("destroy",b,b.collection,a)};if(this.isNew())return d(),false;a.success=function(e){a.wait&&d();c?c(b,e):b.trigger("sync",b,e,a)};a.error=g.wrapError(a.error,b,a);var e=(this.sync||
+g.sync).call(this,"delete",this,a);a.wait||d();return e},url:function(){var a=n(this,"urlRoot")||n(this.collection,"url")||t();return this.isNew()?a:a+(a.charAt(a.length-1)=="/"?"":"/")+encodeURIComponent(this.id)},parse:function(a){return a},clone:function(){return new this.constructor(this.attributes)},isNew:function(){return this.id==null},change:function(a){a||(a={});var b=this._changing;this._changing=true;for(var c in this._silent)this._pending[c]=true;var d=f.extend({},a.changes,this._silent);
+this._silent={};for(c in d)this.trigger("change:"+c,this,this.get(c),a);if(b)return this;for(;!f.isEmpty(this._pending);){this._pending={};this.trigger("change",this,a);for(c in this.changed)!this._pending[c]&&!this._silent[c]&&delete this.changed[c];this._previousAttributes=f.clone(this.attributes)}this._changing=false;return this},hasChanged:function(a){return!arguments.length?!f.isEmpty(this.changed):f.has(this.changed,a)},changedAttributes:function(a){if(!a)return this.hasChanged()?f.clone(this.changed):
+false;var b,c=false,d=this._previousAttributes,e;for(e in a)if(!f.isEqual(d[e],b=a[e]))(c||(c={}))[e]=b;return c},previous:function(a){return!arguments.length||!this._previousAttributes?null:this._previousAttributes[a]},previousAttributes:function(){return f.clone(this._previousAttributes)},isValid:function(){return!this.validate(this.attributes)},_validate:function(a,b){if(b.silent||!this.validate)return true;var a=f.extend({},this.attributes,a),c=this.validate(a,b);if(!c)return true;b&&b.error?
+b.error(this,c,b):this.trigger("error",this,c,b);return false}});var r=g.Collection=function(a,b){b||(b={});if(b.model)this.model=b.model;if(b.comparator)this.comparator=b.comparator;this._reset();this.initialize.apply(this,arguments);a&&this.reset(a,{silent:true,parse:b.parse})};f.extend(r.prototype,l,{model:o,initialize:function(){},toJSON:function(a){return this.map(function(b){return b.toJSON(a)})},add:function(a,b){var c,d,e,g,h,j={},i={},l=[];b||(b={});a=f.isArray(a)?a.slice():[a];for(c=0,d=
+a.length;c<d;c++){if(!(e=a[c]=this._prepareModel(a[c],b)))throw Error("Can't add an invalid model to a collection");g=e.cid;h=e.id;j[g]||this._byCid[g]||h!=null&&(i[h]||this._byId[h])?l.push(c):j[g]=i[h]=e}for(c=l.length;c--;)a.splice(l[c],1);for(c=0,d=a.length;c<d;c++)(e=a[c]).on("all",this._onModelEvent,this),this._byCid[e.cid]=e,e.id!=null&&(this._byId[e.id]=e);this.length+=d;z.apply(this.models,[b.at!=null?b.at:this.models.length,0].concat(a));this.comparator&&this.sort({silent:true});if(b.silent)return this;
+for(c=0,d=this.models.length;c<d;c++)if(j[(e=this.models[c]).cid])b.index=c,e.trigger("add",e,this,b);return this},remove:function(a,b){var c,d,e,g;b||(b={});a=f.isArray(a)?a.slice():[a];for(c=0,d=a.length;c<d;c++)if(g=this.getByCid(a[c])||this.get(a[c])){delete this._byId[g.id];delete this._byCid[g.cid];e=this.indexOf(g);this.models.splice(e,1);this.length--;if(!b.silent)b.index=e,g.trigger("remove",g,this,b);this._removeReference(g)}return this},push:function(a,b){a=this._prepareModel(a,b);this.add(a,
+b);return a},pop:function(a){var b=this.at(this.length-1);this.remove(b,a);return b},unshift:function(a,b){a=this._prepareModel(a,b);this.add(a,f.extend({at:0},b));return a},shift:function(a){var b=this.at(0);this.remove(b,a);return b},get:function(a){return a==null?void 0:this._byId[a.id!=null?a.id:a]},getByCid:function(a){return a&&this._byCid[a.cid||a]},at:function(a){return this.models[a]},where:function(a){return f.isEmpty(a)?[]:this.filter(function(b){for(var c in a)if(a[c]!==b.get(c))return false;
+return true})},sort:function(a){a||(a={});if(!this.comparator)throw Error("Cannot sort a set without a comparator");var b=f.bind(this.comparator,this);this.comparator.length==1?this.models=this.sortBy(b):this.models.sort(b);a.silent||this.trigger("reset",this,a);return this},pluck:function(a){return f.map(this.models,function(b){return b.get(a)})},reset:function(a,b){a||(a=[]);b||(b={});for(var c=0,d=this.models.length;c<d;c++)this._removeReference(this.models[c]);this._reset();this.add(a,f.extend({silent:true},
+b));b.silent||this.trigger("reset",this,b);return this},fetch:function(a){a=a?f.clone(a):{};if(a.parse===void 0)a.parse=true;var b=this,c=a.success;a.success=function(d,e,f){b[a.add?"add":"reset"](b.parse(d,f),a);c&&c(b,d)};a.error=g.wrapError(a.error,b,a);return(this.sync||g.sync).call(this,"read",this,a)},create:function(a,b){var c=this,b=b?f.clone(b):{},a=this._prepareModel(a,b);if(!a)return false;b.wait||c.add(a,b);var d=b.success;b.success=function(e,f){b.wait&&c.add(e,b);d?d(e,f):e.trigger("sync",
+a,f,b)};a.save(null,b);return a},parse:function(a){return a},chain:function(){return f(this.models).chain()},_reset:function(){this.length=0;this.models=[];this._byId={};this._byCid={}},_prepareModel:function(a,b){b||(b={});if(a instanceof o){if(!a.collection)a.collection=this}else{var c;b.collection=this;a=new this.model(a,b);a._validate(a.attributes,b)||(a=false)}return a},_removeReference:function(a){this==a.collection&&delete a.collection;a.off("all",this._onModelEvent,this)},_onModelEvent:function(a,
+b,c,d){(a=="add"||a=="remove")&&c!=this||(a=="destroy"&&this.remove(b,d),b&&a==="change:"+b.idAttribute&&(delete this._byId[b.previous(b.idAttribute)],this._byId[b.id]=b),this.trigger.apply(this,arguments))}});f.each("forEach,each,map,reduce,reduceRight,find,detect,filter,select,reject,every,all,some,any,include,contains,invoke,max,min,sortBy,sortedIndex,toArray,size,first,initial,rest,last,without,indexOf,shuffle,lastIndexOf,isEmpty,groupBy".split(","),function(a){r.prototype[a]=function(){return f[a].apply(f,
+[this.models].concat(f.toArray(arguments)))}});var u=g.Router=function(a){a||(a={});if(a.routes)this.routes=a.routes;this._bindRoutes();this.initialize.apply(this,arguments)},A=/:\w+/g,B=/\*\w+/g,C=/[-[\]{}()+?.,\\^$|#\s]/g;f.extend(u.prototype,l,{initialize:function(){},route:function(a,b,c){g.history||(g.history=new m);f.isRegExp(a)||(a=this._routeToRegExp(a));c||(c=this[b]);g.history.route(a,f.bind(function(d){d=this._extractParameters(a,d);c&&c.apply(this,d);this.trigger.apply(this,["route:"+
+b].concat(d));g.history.trigger("route",this,b,d)},this));return this},navigate:function(a,b){g.history.navigate(a,b)},_bindRoutes:function(){if(this.routes){var a=[],b;for(b in this.routes)a.unshift([b,this.routes[b]]);b=0;for(var c=a.length;b<c;b++)this.route(a[b][0],a[b][1],this[a[b][1]])}},_routeToRegExp:function(a){a=a.replace(C,"\\$&").replace(A,"([^/]+)").replace(B,"(.*?)");return RegExp("^"+a+"$")},_extractParameters:function(a,b){return a.exec(b).slice(1)}});var m=g.History=function(){this.handlers=
+[];f.bindAll(this,"checkUrl")},s=/^[#\/]/,D=/msie [\w.]+/;m.started=false;f.extend(m.prototype,l,{interval:50,getHash:function(a){return(a=(a?a.location:window.location).href.match(/#(.*)$/))?a[1]:""},getFragment:function(a,b){if(a==null)if(this._hasPushState||b){var a=window.location.pathname,c=window.location.search;c&&(a+=c)}else a=this.getHash();a.indexOf(this.options.root)||(a=a.substr(this.options.root.length));return a.replace(s,"")},start:function(a){if(m.started)throw Error("Backbone.history has already been started");
+m.started=true;this.options=f.extend({},{root:"/"},this.options,a);this._wantsHashChange=this.options.hashChange!==false;this._wantsPushState=!!this.options.pushState;this._hasPushState=!(!this.options.pushState||!window.history||!window.history.pushState);var a=this.getFragment(),b=document.documentMode;if(b=D.exec(navigator.userAgent.toLowerCase())&&(!b||b<=7))this.iframe=i('<iframe src="javascript:0" tabindex="-1" />').hide().appendTo("body")[0].contentWindow,this.navigate(a);if(this._hasPushState)i(window).bind("popstate",
+this.checkUrl);else if(this._wantsHashChange&&"onhashchange"in window&&!b)i(window).bind("hashchange",this.checkUrl);else if(this._wantsHashChange)this._checkUrlInterval=setInterval(this.checkUrl,this.interval);this.fragment=a;a=window.location;b=a.pathname==this.options.root;if(this._wantsHashChange&&this._wantsPushState&&!this._hasPushState&&!b)return this.fragment=this.getFragment(null,true),window.location.replace(this.options.root+"#"+this.fragment),true;else if(this._wantsPushState&&this._hasPushState&&
+b&&a.hash)this.fragment=this.getHash().replace(s,""),window.history.replaceState({},document.title,a.protocol+"//"+a.host+this.options.root+this.fragment);if(!this.options.silent)return this.loadUrl()},stop:function(){i(window).unbind("popstate",this.checkUrl).unbind("hashchange",this.checkUrl);clearInterval(this._checkUrlInterval);m.started=false},route:function(a,b){this.handlers.unshift({route:a,callback:b})},checkUrl:function(){var a=this.getFragment();a==this.fragment&&this.iframe&&(a=this.getFragment(this.getHash(this.iframe)));
+if(a==this.fragment)return false;this.iframe&&this.navigate(a);this.loadUrl()||this.loadUrl(this.getHash())},loadUrl:function(a){var b=this.fragment=this.getFragment(a);return f.any(this.handlers,function(a){if(a.route.test(b))return a.callback(b),true})},navigate:function(a,b){if(!m.started)return false;if(!b||b===true)b={trigger:b};var c=(a||"").replace(s,"");if(this.fragment!=c)this._hasPushState?(c.indexOf(this.options.root)!=0&&(c=this.options.root+c),this.fragment=c,window.history[b.replace?
+"replaceState":"pushState"]({},document.title,c)):this._wantsHashChange?(this.fragment=c,this._updateHash(window.location,c,b.replace),this.iframe&&c!=this.getFragment(this.getHash(this.iframe))&&(b.replace||this.iframe.document.open().close(),this._updateHash(this.iframe.location,c,b.replace))):window.location.assign(this.options.root+a),b.trigger&&this.loadUrl(a)},_updateHash:function(a,b,c){c?a.replace(a.toString().replace(/(javascript:|#).*$/,"")+"#"+b):a.hash=b}});var v=g.View=function(a){this.cid=
+f.uniqueId("view");this._configure(a||{});this._ensureElement();this.initialize.apply(this,arguments);this.delegateEvents()},E=/^(\S+)\s*(.*)$/,w="model,collection,el,id,attributes,className,tagName".split(",");f.extend(v.prototype,l,{tagName:"div",$:function(a){return this.$el.find(a)},initialize:function(){},render:function(){return this},remove:function(){this.$el.remove();return this},make:function(a,b,c){a=document.createElement(a);b&&i(a).attr(b);c!=null&&i(a).html(c);return a},setElement:function(a,
+b){this.$el&&this.undelegateEvents();this.$el=a instanceof i?a:i(a);this.el=this.$el[0];b!==false&&this.delegateEvents();return this},delegateEvents:function(a){if(a||(a=n(this,"events"))){this.undelegateEvents();for(var b in a){var c=a[b];f.isFunction(c)||(c=this[a[b]]);if(!c)throw Error('Method "'+a[b]+'" does not exist');var d=b.match(E),e=d[1],d=d[2],c=f.bind(c,this);e+=".delegateEvents"+this.cid;d===""?this.$el.bind(e,c):this.$el.delegate(d,e,c)}}},undelegateEvents:function(){this.$el.unbind(".delegateEvents"+
+this.cid)},_configure:function(a){this.options&&(a=f.extend({},this.options,a));for(var b=0,c=w.length;b<c;b++){var d=w[b];a[d]&&(this[d]=a[d])}this.options=a},_ensureElement:function(){if(this.el)this.setElement(this.el,false);else{var a=n(this,"attributes")||{};if(this.id)a.id=this.id;if(this.className)a["class"]=this.className;this.setElement(this.make(this.tagName,a),false)}}});o.extend=r.extend=u.extend=v.extend=function(a,b){var c=F(this,a,b);c.extend=this.extend;return c};var G={create:"POST",
+update:"PUT","delete":"DELETE",read:"GET"};g.sync=function(a,b,c){var d=G[a];c||(c={});var e={type:d,dataType:"json"};if(!c.url)e.url=n(b,"url")||t();if(!c.data&&b&&(a=="create"||a=="update"))e.contentType="application/json",e.data=JSON.stringify(b.toJSON());if(g.emulateJSON)e.contentType="application/x-www-form-urlencoded",e.data=e.data?{model:e.data}:{};if(g.emulateHTTP&&(d==="PUT"||d==="DELETE")){if(g.emulateJSON)e.data._method=d;e.type="POST";e.beforeSend=function(a){a.setRequestHeader("X-HTTP-Method-Override",
+d)}}if(e.type!=="GET"&&!g.emulateJSON)e.processData=false;return i.ajax(f.extend(e,c))};g.wrapError=function(a,b,c){return function(d,e){e=d===b?e:d;a?a(b,e,c):b.trigger("error",b,e,c)}};var x=function(){},F=function(a,b,c){var d;d=b&&b.hasOwnProperty("constructor")?b.constructor:function(){a.apply(this,arguments)};f.extend(d,a);x.prototype=a.prototype;d.prototype=new x;b&&f.extend(d.prototype,b);c&&f.extend(d,c);d.prototype.constructor=d;d.__super__=a.prototype;return d},n=function(a,b){return!a||
+!a[b]?null:f.isFunction(a[b])?a[b]():a[b]},t=function(){throw Error('A "url" property or function must be specified');};return g});
View
42 public/javascripts/lib/can.construct.super.js
@@ -1,42 +0,0 @@
-define(['can'], function(can) {
- // tests if we can get super in .toString()
- var isFunction = can.isFunction,
-
- fnTest = /xyz/.test(function() {
- xyz;
- }) ? /\b_super\b/ : /.*/;
-
- // overwrites a single property so it can still call super
- can.Construct._overwrite = function(addTo, base, name, val){
- // Check if we're overwriting an existing function
- addTo[name] = isFunction(val) &&
- isFunction(base[name]) &&
- fnTest.test(val) ? (function( name, fn ) {
- return function() {
- var tmp = this._super,
- ret;
-
- // Add a new ._super() method that is the same method
- // but on the super-class
- this._super = base[name];
-
- // The method only need to be bound temporarily, so we
- // remove it when we're done executing
- ret = fn.apply(this, arguments);
- this._super = tmp;
- return ret;
- };
- })(name, val) : val;
- };
-
- // overwrites an object with methods, sets up _super
- // newProps - new properties
- // oldProps - where the old properties might be
- // addTo - what we are adding to
- can.Construct._inherit = function( newProps, oldProps, addTo ) {
- addTo = addTo || newProps;
- for ( var name in newProps ) {
- can.Construct._overwrite(addTo, oldProps, name, newProps[name]);