diff --git a/app/components/file-editor-column.js b/app/components/file-editor-column.js index 8aac2586..62ed54ae 100644 --- a/app/components/file-editor-column.js +++ b/app/components/file-editor-column.js @@ -1,5 +1,8 @@ import Ember from 'ember'; +const { computed } = Ember; +const MAX_COLUMNS = 3; + export default Ember.Component.extend({ focusEditor: 'focusEditor', selectFile: 'selectFile', @@ -18,6 +21,11 @@ export default Ember.Component.extend({ } }), + isLastColumn: computed('col', 'numColumns', function() { + let numColumns = this.get('numColumns'); + return (this.get('col') | 0) === numColumns && numColumns < MAX_COLUMNS; + }), + focusIn () { this.sendAction('focusEditor', this); }, @@ -30,6 +38,14 @@ export default Ember.Component.extend({ valueUpdated() { this.sendAction('contentsChanged'); + }, + + removeColumn(col) { + this.attrs.removeColumn(col); + }, + + addColumn() { + this.attrs.addColumn(); } } }); diff --git a/app/gist/controller.js b/app/gist/controller.js index 13de0785..b4f2cd0b 100644 --- a/app/gist/controller.js +++ b/app/gist/controller.js @@ -2,18 +2,25 @@ import Ember from "ember"; import config from '../config/environment'; import Settings from '../models/settings'; import ErrorMessages from 'ember-twiddle/helpers/error-messages'; +import Column from '../utils/column'; const { - computed: { equal }, - observer, + computed, run } = Ember; +const MAX_COLUMNS = 3; + export default Ember.Controller.extend({ emberCli: Ember.inject.service('ember-cli'), version: config.APP.version, + + queryParams: ['numColumns'], + numColumns: 2, + init() { this._super(...arguments); + this.createColumns(); this.setupWindowUpdate(); }, @@ -22,14 +29,32 @@ export default Ember.Controller.extend({ * @type {String} */ buildOutput: '', + + /** + * If the code is currently being built + * @type {boolean} + */ isBuilding: false, + + /** + * If the edited code has not been saved by the user + * @type {boolean} + */ unsaved: true, + + /** + * File in the current active editor column + * @type {Object} + */ activeFile: null, + + /** + * Column which has the currently focused editor + * @type {Number} + */ activeEditorCol: null, - col1File: null, - col2File: null, - col1Active: equal('activeEditorCol','1'), - col2Active: equal('activeEditorCol','2'), + + columns: null, settings: Settings.create(), @@ -69,20 +94,69 @@ export default Ember.Controller.extend({ }); }, + realNumColumns: computed('numColumns', function() { + return Math.min(this.get('numColumns'), MAX_COLUMNS); + }), + noColumns: computed.equal('numColumns', 0), + /** - * Set the initial file columns + * Creates the column objects */ - initializeColumns: observer('model', function() { - var files = this.get('model.files'); + createColumns() { + let columns = []; + for (let i = 0; i < MAX_COLUMNS; ++i) { + let col = (i + 1) + ""; + columns.pushObject(Column.create({ + col: col, + controller: this + })); + } + this.set('columns', columns); + }, - if(files.objectAt(0)) { - this.setColumnFile(1, files.objectAt(0)); + /** + * Set the initial files in the columns + */ + initializeColumns() { + let files = this.get('model.files'); + let numColumns = this.get('realNumColumns'); + + let j = 0; + for (let i = 1; i <= numColumns; ++i) { + if (!this.getColumnFile(i)) { + if (files) { + j = 0; + while (!this.isOpen(files.objectAt(j))) { + j++; + } + let file = files.objectAt(j); + if (file) { + this.setColumnFile(i, file); + } + } + } } + }, - if(files.objectAt(1)) { - this.setColumnFile(2, files.objectAt(1)); + /** + * Returns true if the passed in file is currently open + * @param {Object} one of the files in the gist + * @return {boolean} + */ + isOpen(file) { + if (!file) { + return false; + } + + for (let i = 1; i <= MAX_COLUMNS; ++i) { + let colFile = this.getColumnFile(i); + if (colFile && colFile.get('fileName') === file.get('fileName')){ + return false; + } } - }), + + return true; + }, rebuildApp: function() { if (this.get('isLiveReload')) { @@ -107,6 +181,7 @@ export default Ember.Controller.extend({ this.send('contentsChanged'); } }, + /* * Test whether path is valid. Presently only tests whether components are hyphenated. */ @@ -123,9 +198,13 @@ export default Ember.Controller.extend({ } return false; }, - setColumnFile(column, file){ - let fileColumn = `col${column}File`; - this.set(fileColumn, file); + + getColumnFile(column) { + return this.get('columns').objectAt(column - 1).get('file'); + }, + + setColumnFile(column, file) { + this.get('columns').objectAt(column - 1).set('file', file); }, actions: { @@ -179,6 +258,7 @@ export default Ember.Controller.extend({ this.createFile(filePath, fileProperties, fileColumn); }); }, + /** * Add a new file to the model * @param {String|null} type Blueprint name or null for empty file @@ -225,6 +305,32 @@ export default Ember.Controller.extend({ } }, + removeColumn (col) { + let numColumns = this.get('realNumColumns'); + + for (var i = (col|0); i < numColumns; ++i) { + this.setColumnFile(i, this.getColumnFile(i + 1)); + } + this.setColumnFile(numColumns, undefined); + + let activeCol = this.get('activeEditorCol'); + if (activeCol >= col) { + this.set('activeEditorCol', ((activeCol|0) - 1).toString()); + } + + this.transitionToRoute({queryParams: {numColumns: numColumns - 1}}); + }, + + addColumn() { + let numColumns = this.get('realNumColumns'); + + this.transitionToRoute({ + queryParams: { + numColumns: numColumns + 1 + } + }).then(this.initializeColumns.bind(this)); + }, + setEditorKeyMap (keyMap) { const settings = this.get('settings'); settings.set('keyMap', keyMap); @@ -233,11 +339,10 @@ export default Ember.Controller.extend({ }, _removeFileFromColumns (file) { - if(this.get('col1File') === file) { - this.setColumnFile(1, null); - } - if(this.get('col2File') === file) { - this.setColumnFile(2, null); + for (let i = 1; i <= MAX_COLUMNS; ++i) { + if (this.getColumnFile(i) === file) { + this.setColumnFile(i, null); + } } }, diff --git a/app/gist/template.hbs b/app/gist/template.hbs index 4b3348fc..dc1d46ce 100644 --- a/app/gist/template.hbs +++ b/app/gist/template.hbs @@ -17,9 +17,7 @@
- {{!-- {{#if isEditingDescription}} --}} {{title-input value=model.description}} - {{!-- {{model.description}} --}} {{saved-state-indicator model=model unsaved=unsaved }} @@ -33,17 +31,27 @@
- -
- {{file-editor-column col="1" file=col1File allFiles=model.files keyMap=settings.keyMap contentsChanged="contentsChanged"}} -
- -
- {{file-editor-column col="2" file=col2File allFiles=model.files keyMap=settings.keyMap contentsChanged="contentsChanged"}} -
+ {{#each columns as |column|}} + {{#if column.show}} +
+ {{file-editor-column col=column.col + file=column.file + allFiles=model.files + keyMap=settings.keyMap + numColumns=realNumColumns + contentsChanged="contentsChanged" + removeColumn=(action "removeColumn") + addColumn=(action "addColumn") + }} +
+ {{/if}} + {{/each}}
+ {{#if noColumns}} + + {{/if}} {{build-messages notify=notify isBuilding=isBuilding buildErrors=buildErrors diff --git a/app/routes/gist-base-route.js b/app/routes/gist-base-route.js index f9e6706d..6ff65c65 100644 --- a/app/routes/gist-base-route.js +++ b/app/routes/gist-base-route.js @@ -2,6 +2,8 @@ import Ember from 'ember'; export default Ember.Route.extend({ setupController (controller, context) { - this.controllerFor('gist').set('model', context); + let gistController = this.controllerFor('gist'); + gistController.set('model', context); + gistController.initializeColumns(); } }); diff --git a/app/styles/app.scss b/app/styles/app.scss index a3f75476..0a1f3586 100644 --- a/app/styles/app.scss +++ b/app/styles/app.scss @@ -200,8 +200,10 @@ body { margin: 0; margin-left: 0; z-index: -1; + display: flex; .col-md-4 { + flex-grow: 1; padding: 0; height: 100%; border-right: 1px solid #e4e2e2; @@ -221,6 +223,10 @@ body { height: 100%; position: absolute; } + + span.glyphicon-plus { + cursor: pointer; + } } .header { @@ -228,6 +234,7 @@ body { border-bottom: 1px solid #e4e2e2; font-size: 16px; background-color: #faf4f1; + position: relative; .file-picker { margin-left: 15px; @@ -235,6 +242,17 @@ body { margin-bottom: 0; position: absolute; } + + .nav-right { + position: absolute; + top: 20px; + right: 15px; + + .glyphicon { + color: $navbar-default-link-color; + cursor: pointer; + } + } } } diff --git a/app/templates/components/file-editor-column.hbs b/app/templates/components/file-editor-column.hbs index 942478ba..f42ba349 100644 --- a/app/templates/components/file-editor-column.hbs +++ b/app/templates/components/file-editor-column.hbs @@ -22,6 +22,13 @@ + + + + {{#if isLastColumn}} + + {{/if}} +
{{#if file}} diff --git a/app/utils/column.js b/app/utils/column.js new file mode 100644 index 00000000..4f52f20f --- /dev/null +++ b/app/utils/column.js @@ -0,0 +1,15 @@ +import Ember from "ember"; + +const { computed } = Ember; + +export default Ember.Object.extend ({ + col: "", + controller: null, + active: computed('controller.activeEditorCol', 'col', function() { + return this.get('controller.activeEditorCol') === this.get('col'); + }), + show: computed('controller.realNumColumns', 'col', function() { + return this.get('controller.realNumColumns') >= this.get('col'); + }), + file: null +}); diff --git a/tests/acceptance/columns-test.js b/tests/acceptance/columns-test.js new file mode 100644 index 00000000..d5532e55 --- /dev/null +++ b/tests/acceptance/columns-test.js @@ -0,0 +1,65 @@ +import Ember from 'ember'; +import { module, test } from 'qunit'; +import startApp from 'ember-twiddle/tests/helpers/start-app'; + +module('Acceptance | columns', { + beforeEach: function() { + this.application = startApp(); + + server.create('user', 'octocat'); + }, + + afterEach: function() { + Ember.run(this.application, 'destroy'); + } +}); + +const columns = ".code"; +const firstColumn = ".code:first-of-type"; +const plusGlyph = ".code .glyphicon-plus"; +const removeGlyph = firstColumn + " .glyphicon-remove"; +const outputPlusGlyph = ".output .glyphicon-plus"; + +test('you can add and remove columns', function(assert) { + visit('/'); + + andThen(function() { + assert.equal(currentURL(), '/', 'We are on the correct initial route'); + assert.equal(find(columns).length, 2, 'There are two columns to start'); + + find(plusGlyph).click(); + }); + + andThen(function() { + assert.equal(currentURL(), '/?numColumns=3', 'We are on the correct route for 3 columns'); + assert.equal(find(columns).length, 3, 'There are now 3 columns'); + + find(removeGlyph).click(); + }); + + andThen(function() { + assert.equal(currentURL(), '/', 'We are on the correct route for 2 columns'); + assert.equal(find(columns).length, 2, 'There are now 2 columns'); + + find(removeGlyph).click(); + }); + + andThen(function() { + assert.equal(currentURL(), '/?numColumns=1', 'We are on the correct route for 1 columns'); + assert.equal(find(columns).length, 1, 'There are now 1 columns'); + + find(removeGlyph).click(); + }); + + andThen(function() { + assert.equal(currentURL(), '/?numColumns=0', 'We are on the correct route for 0 columns'); + assert.equal(find(columns).length, 0, 'There are now 0 columns'); + + find(outputPlusGlyph).click(); + }); + + andThen(function() { + assert.equal(currentURL(), '/?numColumns=1', 'We are on the correct route for 1 columns'); + assert.equal(find(columns).length, 1, 'There are now 1 columns'); + }); +}); diff --git a/tests/integration/components/file-editor-column-test.js b/tests/integration/components/file-editor-column-test.js new file mode 100644 index 00000000..6fee347b --- /dev/null +++ b/tests/integration/components/file-editor-column-test.js @@ -0,0 +1,39 @@ +import { moduleForComponent, test } from 'ember-qunit'; +import hbs from 'htmlbars-inline-precompile'; + +moduleForComponent('file-editor-column', 'Integration | Component | file editor column', { + integration: true, + beforeEach() { + this.addColumnCalled = false; + this.removeColumnCalled = false; + + this.on('addColumn', () => { this.addColumnCalled = true; }); + this.on('removeColumn', () => { this.removeColumnCalled = true; }); + + this.render(hbs`{{file-editor-column col="2" + file=null + allFiles=null + keyMap=null + numColumns=2 + contentsChanged="contentsChanged" + addColumn=(action "addColumn") + removeColumn=(action "removeColumn") + }}`); + } +}); + +test('it calls addColumn when the add column glyph is clicked', function(assert) { + assert.expect(1); + + this.$('.glyphicon-plus').click(); + + assert.ok(this.addColumnCalled, 'addColumn was called'); +}); + +test('it calls removeColumn when the remove column glyph is clicked', function(assert) { + assert.expect(1); + + this.$('.glyphicon-remove').click(); + + assert.ok(this.removeColumnCalled, 'removeColumn was called'); +}); diff --git a/tests/unit/utils/column-test.js b/tests/unit/utils/column-test.js new file mode 100644 index 00000000..572e2bb9 --- /dev/null +++ b/tests/unit/utils/column-test.js @@ -0,0 +1,31 @@ +import Ember from "ember"; +import Column from '../../../utils/column'; +import { module, test } from 'qunit'; + +module('Unit | Utility | column'); + +test('Active Column matches', function(assert) { + assert.expect(2); + var controller = Ember.Object.create({ + activeEditorCol: '2', + realNumColumns: 3 + }); + var column1 = Column.create({ col: '1', controller: controller }); + assert.ok(!column1.get('active')); + var column2 = Column.create({ col: '2', controller: controller }); + assert.ok(column2.get('active')); +}); + +test('Show Column if less than or equal to realNumColumns', function(assert) { + assert.expect(3); + var controller = Ember.Object.create({ + activeEditorCol: '3', + realNumColumns: 2 + }); + var column1 = Column.create({ col: '1', controller: controller }); + assert.ok(column1.get('show')); + var column2 = Column.create({ col: '2', controller: controller }); + assert.ok(column2.get('show')); + var column3 = Column.create({ col: '3', controller: controller }); + assert.ok(!column3.get('show')); +});