Comments UI

- Added comments resource
- Added comment list controller to resolve author names.
- Updated detail controller to allow comment control.
- Major rework of the story UI to make room for a tasks and discussions.
- Refactor of story controllers to separate concernse into tasks, comments, story.
- Collapse of the story statechart.
- New LESS file for discussions.
- New directive: ngShiftEnter, to allow shift-enter comment submission.
- New directive: contenteditable, to allow in-context editing.

Change-Id: I8be85da39097683eb7660835c1cbc40524dd5b2c
This commit is contained in:
Michael Krotscheck 2014-03-21 14:18:03 -07:00
parent 5751ac7eb9
commit f0232defc3
16 changed files with 685 additions and 376 deletions

View File

@ -0,0 +1,33 @@
/*
* Copyright (c) 2014 Hewlett-Packard Development Company, L.P.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License. You may obtain
* a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*/
/**
* The angular resource abstraction that allows us to access discussions that
* are surrounding stories.
*
* @see storyboardApiSignature
*/
angular.module('sb.services').factory('Comment',
function ($resource, storyboardApiBase, storyboardApiSignature) {
'use strict';
return $resource(storyboardApiBase + '/stories/:story_id/comments/:id',
{
id: '@id',
story_id: '@story_id'
},
storyboardApiSignature);
});

View File

@ -0,0 +1,42 @@
/*
* Copyright (c) 2014 Hewlett-Packard Development Company, L.P.
*
* Licensed under the Apache License, Version 2.0 (the 'License'); you may
* not use this file except in compliance with the License. You may obtain
* a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an 'AS IS' BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*/
/**
* Story detail & manipulation controller.
*/
angular.module('sb.story').controller('StoryDeleteController',
function ($log, $scope, $state, story, $modalInstance) {
'use strict';
$scope.story = story;
// Set our progress flags and clear previous error conditions.
$scope.isUpdating = true;
$scope.error = {};
$scope.remove = function () {
$scope.story.$delete(
function () {
$modalInstance.dismiss('success');
$state.go('project.list');
}
);
};
$scope.close = function () {
$modalInstance.dismiss('cancel');
};
});

View File

@ -1,14 +1,14 @@
/*
* Copyright (c) 2014 Hewlett-Packard Development Company, L.P.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may
* Licensed under the Apache License, Version 2.0 (the 'License'); you may
* not use this file except in compliance with the License. You may obtain
* a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* distributed under the License is distributed on an 'AS IS' BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
@ -18,7 +18,7 @@
* Story detail & manipulation controller.
*/
angular.module('sb.story').controller('StoryDetailController',
function ($scope, $state, $stateParams, Story, Task, Project) {
function ($log, $scope, $state, $stateParams, $modal, Story) {
'use strict';
// Parse the ID
@ -26,15 +26,33 @@ angular.module('sb.story').controller('StoryDetailController',
parseInt($stateParams.storyId, 10) :
null;
/**
* Generic service error handler. Assigns errors to the view's scope,
* and unsets our flags.
*/
function handleServiceError(error) {
// We've encountered an error.
$scope.error = error;
$scope.isLoading = false;
$scope.isUpdating = false;
}
/**
* Resets our loading flags.
*/
function handleServiceSuccess() {
$scope.isLoading = false;
$scope.isUpdating = false;
}
/**
* The story we're manipulating right now.
*/
$scope.story = {};
$scope.tasks = [];
$scope.newTask = new Task({
story_id: id
});
$scope.projects = Project.query({});
$scope.story = Story.get(
{'id': id},
handleServiceSuccess,
handleServiceError
);
/**
* UI flag for when we're initially loading the view.
@ -58,71 +76,7 @@ angular.module('sb.story').controller('StoryDetailController',
$scope.error = {};
/**
* Generic service error handler. Assigns errors to the view's scope,
* and unsets our flags.
*/
function handleServiceError(error) {
// We've encountered an error.
$scope.error = error;
$scope.isLoading = false;
$scope.isUpdating = false;
}
/**
* Loads the tasks for this story
*/
function loadTasks() {
$scope.tasks = [];
Task.query(
{story_id: id},
function (result) {
$scope.tasks = result;
},
handleServiceError
);
}
// Sanity check, do we actually have an ID? (zero is falsy)
if (!id && id !== 0) {
// We should never reach this, however that logic lives outside
// of this controller which could be unknowningly refactored.
$scope.error = {
error: true,
error_code: 404,
error_message: 'You did not provide a valid ID.'
};
$scope.isLoading = false;
} else {
// We've got an ID, so let's load it...
Story.read(
{'id': id},
function (result) {
// We've got a result, assign it to the view and unset our
// loading flag.
$scope.story = result;
$scope.newTask.project_id = result.project_id;
$scope.isLoading = false;
},
handleServiceError
);
loadTasks();
}
/**
* Adds a task.
*/
$scope.addTask = function () {
$scope.newTask.$save(function () {
loadTasks();
$scope.newTask = new Task();
});
};
/**
* Scope method, invoke this when you want to update the project.
* Scope method, invoke this when you want to update the story.
*/
$scope.update = function () {
// Set our progress flags and clear previous error conditions.
@ -130,29 +84,25 @@ angular.module('sb.story').controller('StoryDetailController',
$scope.error = {};
// Invoke the save method and wait for results.
$scope.story.$update(
function (result) {
// Unset our loading flag and navigate to the detail view.
$scope.isUpdating = false;
$state.go('story.detail.overview', {storyId: result.id});
},
handleServiceError
);
$scope.story.$update(handleServiceSuccess, handleServiceError);
};
/**
* Delete method.
*/
$scope.remove = function () {
// Set our progress flags and clear previous error conditions.
$scope.isUpdating = true;
$scope.error = {};
$scope.story.$delete(
function () {
$state.go('project.list');
},
handleServiceError
);
var modalInstance = $modal.open({
templateUrl: 'app/templates/story/delete.html',
controller: 'StoryDeleteController',
resolve: {
story: function () {
return $scope.story;
}
}
});
// Return the modal's promise.
return modalInstance.result;
};
});

View File

@ -0,0 +1,88 @@
/*
* Copyright (c) 2014 Hewlett-Packard Development Company, L.P.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License. You may obtain
* a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*/
/**
* Controller used for the comments section on the story detail page.
*/
angular.module('sb.story').controller('StoryDiscussionController',
function ($log, $scope, $state, $stateParams, Project, Comment) {
'use strict';
// Parse the ID
var id = $stateParams.hasOwnProperty('storyId') ?
parseInt($stateParams.storyId, 10) :
null;
/**
* The story we're manipulating right now.
*/
$scope.comments = Comment.query({story_id: id});
/**
* The new comment backing the input form.
*/
$scope.newComment = new Comment({story_id: id});
/**
* UI flag for when we're initially loading the view.
*
* @type {boolean}
*/
$scope.isLoading = true;
/**
* UI view for when we're trying to save a comment.
*
* @type {boolean}
*/
$scope.isSavingComment = false;
/**
* Any error objects returned from the services.
*
* @type {{}}
*/
$scope.error = {};
/**
* Add a comment
*/
$scope.addComment = function () {
function resetSavingFlag() {
$scope.isSavingComment = false;
}
// Do nothing if the comment is empty
if (!$scope.newComment.content) {
$log.warn('No content in comment, discarding submission');
return;
}
$scope.isSavingComment = true;
// Author ID will be automatically attached by the service, so
// don't inject it into the conversation until it comes back.
$scope.newComment.$create(
function (comment) {
$scope.comments.push(comment);
$scope.newComment = new Comment({story_id: id});
resetSavingFlag();
},
resetSavingFlag
);
};
});

View File

@ -0,0 +1,27 @@
/*
* Copyright (c) 2014 Hewlett-Packard Development Company, L.P.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License. You may obtain
* a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*/
/**
* Controller used for the comments section on the story detail page.
*/
angular.module('sb.story').controller('StoryDiscussionItemController',
function ($scope, User) {
'use strict';
$scope.author = User.get({
id: $scope.comment.author_id
});
});

View File

@ -0,0 +1,98 @@
/*
* Copyright (c) 2014 Hewlett-Packard Development Company, L.P.
*
* Licensed under the Apache License, Version 2.0 (the 'License'); you may
* not use this file except in compliance with the License. You may obtain
* a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an 'AS IS' BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*/
/**
* Controller that provides methods that allow editing of a story.
*/
angular.module('sb.story').controller('StoryTaskListController',
function ($log, $scope, $state, $stateParams, Task, Project) {
'use strict';
// Parse the ID
var id = $stateParams.hasOwnProperty('storyId') ?
parseInt($stateParams.storyId, 10) :
null;
/**
* The current list of tasks
*/
$scope.tasks = [];
/**
* Display toggle for the add task form.
*
* @TODO(krotscheck) Remove, we're not using this interface pattern
* anywhere else. Should probably be a modal...?
*/
$scope.showAddTaskForm = false;
/**
* The new task for the task form.
*/
$scope.newTask = new Task({ story_id: id });
/**
* Projects for the new task form
*/
$scope.projects = Project.query();
/**
* UI flag for when we're initially loading the view.
*
* @type {boolean}
*/
$scope.isLoading = true;
/**
* Any error objects returned from the services.
*
* @type {{}}
*/
$scope.error = {};
/**
* Loads the tasks for this story
*/
function loadTasks() {
$scope.tasks = [];
Task.query(
{story_id: id},
function (result) {
$scope.tasks = result;
},
function (error) {
// We've encountered an error.
$scope.error = error;
$scope.isLoading = false;
$scope.isUpdating = false;
}
);
}
/**
* Adds a task.
*/
$scope.addTask = function () {
$scope.newTask.$save(function () {
loadTasks();
$scope.newTask = new Task({story_id: id});
});
};
// Initialize our view
loadTasks();
});

View File

@ -18,25 +18,12 @@
* The Storyboard story submodule handles most activity surrounding the
* creation and management of stories, their tasks, and comments.
*/
angular.module('sb.story', ['ui.router', 'sb.services', 'sb.util', 'sb.auth'])
.config(function ($stateProvider, $urlRouterProvider, SessionResolver) {
angular.module('sb.story', ['ui.router', 'sb.services', 'sb.util'])
.config(function ($stateProvider, $urlRouterProvider) {
'use strict';
// URL Defaults.
$urlRouterProvider.when('/story', '/story/list');
$urlRouterProvider.when('/story/{id:[0-9]+}',
function ($match) {
return '/story/' + $match.id + '/overview';
});
$urlRouterProvider.when('/story/{storyId:[0-9]+}/task',
function ($match) {
return '/story/' + $match.storyId + '/overview';
});
$urlRouterProvider.when('/story/{storyId:[0-9]+}/task/{taskId:[0-9]+}',
function ($match) {
return '/story/' + $match.storyId +
'/task/' + $match.taskId;
});
// Set our page routes.
$stateProvider
@ -52,26 +39,6 @@ angular.module('sb.story', ['ui.router', 'sb.services', 'sb.util', 'sb.auth'])
})
.state('story.detail', {
url: '/{storyId:[0-9]+}',
abstract: true,
templateUrl: 'app/templates/story/detail.html',
controller: 'StoryDetailController'
})
.state('story.detail.overview', {
url: '/overview',
templateUrl: 'app/templates/story/overview.html'
})
.state('story.detail.edit', {
url: '/edit',
templateUrl: 'app/templates/story/edit.html',
resolve: {
isLoggedIn: SessionResolver.requireLoggedIn
}
})
.state('story.detail.delete', {
url: '/delete',
templateUrl: 'app/templates/story/delete.html',
resolve: {
isLoggedIn: SessionResolver.requireLoggedIn
}
templateUrl: 'app/templates/story/detail.html'
});
});

View File

@ -13,22 +13,29 @@
~ License for the specific language governing permissions and limitations
~ under the License.
-->
<div class="panel panel-default">
<div class="panel-heading">
<button type="button" class="close" aria-hidden="true"
ng-click="close()">&times;</button>
<h3 class="panel-title">{{story.title}}</h3>
</div>
<div class="panel-body">
<div class="row">
<div class="col-sm-8 col-sm-offset-2">
<h2 class="text-danger text-center">
Are you certain that you want to delete this story?
</h2>
<div class="row">
<div class="col-sm-8 col-sm-offset-2">
<h2 class="text-danger text-center">
Are you certain that you want to delete this story?
</h2>
<p class="text-center lead">
This action cannot be undone.
</p>
<p class="text-center lead">
This will set the story to a "deleted" state, and any
tasks will no longer be visible.
</p>
<div class="text-center">
<a href="" class="btn btn-danger" ng-click="remove()">
Remove this story
</a>
<div class="text-center">
<a href="" class="btn btn-danger" ng-click="remove()">
Remove this story
</a>
</div>
</div>
</div>
</div>
</div>
</div>

View File

@ -13,29 +13,181 @@
~ License for the specific language governing permissions and limitations
~ under the License.
-->
<div class="container" ng-controller="StoryDetailController">
<div class="container">
<h1 class="no-border no-margin-bottom">Story detail: {{story.title}}</h1>
<ul class="nav nav-tabs nav-tabs-down nav-thick">
<li active-path="^\/story\/[0-9]+\/overview.*">
<a href="#!/story/{{story.id}}/overview">
Overview
</a>
</li>
<li active-path="^\/story\/[0-9]+\/edit.*"
ng-show="isLoggedIn">
<a href="#!/story/{{story.id}}/edit">
Edit
</a>
</li>
<li active-path="^\/story\/[0-9]+\/delete.*"
ng-show="isLoggedIn">
<a href="#!/story/{{story.id}}/delete">
Delete
</a>
</li>
</ul>
<br/>
<!-- Begin header -->
<div class="row">
<div class="col-md-8 col-xs-12">
<h1 contenteditable="{{isLoggedIn}}"
ng-model="story.title"></h1>
</div>
</div>
<div ui-view></div>
</div>
<div class="row">
<div class="col-md-8 col-xs-12">
<!-- Begin Description -->
<p contenteditable="{{isLoggedIn}}"
ng-model="story.description">
</p>
<div class="clearfix">
<div class="pull-right">
<div class="btn" ng-show="isUpdating">
<i class="fa fa-spinner fa-lg fa-spin"></i>
</div>
<button type="button"
class="btn btn-primary"
ng-click="update()"
ng-disabled="story.description.length < 1 ||
story.title.length < 1"
ng-show="isLoggedIn">
Save
</button>
<button type="button"
class="btn btn-default"
ng-click="remove()"
ng-show="isLoggedIn">
Delete
</button>
</div>
</div>
<hr/>
<!-- Begin Discussion -->
<div ng-controller="StoryDiscussionController">
<h4>Discussion</h4>
<div class="discussion">
<div class="alert alert-warning"
ng-show="comments.length == 0">
The discussion hasn't started yet
</div>
<div ng-repeat="comment in comments"
ng-controller="StoryDiscussionItemController"
class="discussion-comment">
<p class="discussion-comment-author">
{{author.full_name}}
</p>
<p ng-show="comment.content">{{comment.content}}</p>
<p><em ng-hide="comment.content"
class="text-muted">
The author left a blank comment.
</em></p>
</div>
<form class="discussion-comment-form comment"
id="commentForm"
name="commentForm"
ng-show="isLoggedIn">
<div class="form-group">
<textarea id="comment"
placeholder="Enter your comment here"
class="form-control"
rows="3"
required
ng-disabled="isSavingComment"
ng-shift-enter="addComment()"
ng-model="newComment.content">
</textarea>
</div>
<div class="form-group clearfix">
<button type="button"
class="btn btn-primary pull-right"
ng-click="addComment()"
ng-disabled="!commentForm.$valid || isSavingComment">
Comment
</button>
</div>
</form>
</div>
</div>
<hr class="hidden-md hidden-lg"/>
</div>
<!-- Begin Task List -->
<div class="col-xs-12 col-md-4" ng-controller="StoryTaskListController">
<button type="button"
ng-click="showAddTaskForm = !showAddTaskForm"
ng-show="isLoggedIn"
class="pull-right btn btn-default btn-sm">
<i class="fa fa-plus"
ng-hide="showAddTaskForm"></i>
<i class="fa fa-minus"
ng-show="showAddTaskForm"></i>
</button>
<h4>Tasks</h4>
<div class="well" ng-show="showAddTaskForm">
<form role="form" name="taskForm">
<div class="form-group row">
<label for="title" class="col-sm-2 control-label">
Task Title:
</label>
<div class="col-sm-10">
<input id="title"
type="text"
class="form-control"
ng-model="newTask.title"
required
placeholder="Task Title">
</div>
</div>
<div class="form-group row">
<label for="project" class="col-sm-2 control-label">
Task Project:
</label>
<div class="col-sm-10">
<select ng-model="newTask.project_id"
id="project"
name="project"
class="form-control"
required
ng-options="p.id as p.name for p in projects"/>
</div>
</div>
<div class="row">
<div class="col-sm-offset-2 col-sm-10">
<button type="submit" class="btn btn-default"
ng-click="addTask()"
ng-disabled="!taskForm.$valid">
Add task
</button>
</div>
</div>
</form>
</div>
<table class="table table-striped">
<tbody>
<tr ng-repeat="task in tasks"
ng-controller="StoryTaskListItemController">
<td>
<span class="label label-default pull-right">
{{task.status}}
</span>
<p><strong>
<a href="">{{task.title}}</a>
</strong></p>
<a href="#!/project/{{project.id}}/overview"
ng-show="project">
{{project.name}}
</a>
<span class="text-danger" ng-hide="project">
Project not found
</span>
<small class="text-muted">{{task.description}}
</small>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>

View File

@ -1,64 +0,0 @@
<!--
~ Copyright (c) 2014 Hewlett-Packard Development Company, L.P.
~
~ Licensed under the Apache License, Version 2.0 (the "License"); you may
~ not use this file except in compliance with the License. You may obtain
~ a copy of the License at
~
~ http://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing, software
~ distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
~ WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
~ License for the specific language governing permissions and limitations
~ under the License.
-->
<div class="row">
<div class="col-xs-12">
<form class="form-horizontal" role="form" name="storyForm">
<div class="form-group">
<label for="title" class="col-sm-2 control-label">
Title:
</label>
<div class="col-sm-10">
<input id="title"
type="text"
class="form-control"
ng-model="story.title"
required
placeholder="Story Title">
</div>
</div>
<div class="form-group">
<label for="description"
class="col-sm-2 control-label">
Story Description
</label>
<div class="col-sm-10">
<textarea id="description"
class="form-control"
ng-model="story.description"
required
placeholder="A brief story description">
</textarea>
</div>
</div>
<div class="form-group">
<div class="col-sm-offset-2 col-sm-10">
<button type="button"
ng-click="update()"
class="btn btn-primary"
ng-disabled="!storyForm.$valid">
Save Changes
</button>
<a href="#!/story/list"
class="btn btn-default">
Cancel
</a>
</div>
</div>
</form>
</div>
</div>

View File

@ -1,144 +0,0 @@
<!--
~ Copyright (c) 2014 Hewlett-Packard Development Company, L.P.
~
~ Licensed under the Apache License, Version 2.0 (the "License"); you may
~ not use this file except in compliance with the License. You may obtain
~ a copy of the License at
~
~ http://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing, software
~ distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
~ WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
~ License for the specific language governing permissions and limitations
~ under the License.
-->
<div class="row">
<div class="col-sm-9 col-xs-11">
<p ng-show="story.description">
{{story.description}}
</p>
<em ng-hide="story.description"
class="text-muted">
No description available
</em>
</div>
<div class="col-sm-3 col-xs-1">
<a href=""
ng-click="showAddTaskForm = !showAddTaskForm"
ng-show="isLoggedIn"
class="pull-right btn btn-default btn-sm">
<span ng-hide="showAddTaskForm">
<i class="fa fa-plus"></i>&nbsp;
<span class="hidden-xs">Add Task</span>
</span>
<span ng-show="showAddTaskForm">
<i class="fa fa-minus"></i>&nbsp;
<span class="hidden-xs">Add Task</span>
</span>
</a>
</div>
</div>
<div class="row" ng-show="showAddTaskForm">
<br/>
<div class="col-sm-12">
<div class="well">
<form role="form" name="taskForm">
<div class="form-group row">
<label for="title" class="col-sm-2 control-label">
Task Title:
</label>
<div class="col-sm-10">
<input id="title"
type="text"
class="form-control"
ng-model="newTask.title"
required
placeholder="Task Title">
</div>
</div>
<div class="form-group row">
<label for="project" class="col-sm-2 control-label">
Task Project:
</label>
<div class="col-sm-10">
<select ng-model="newTask.project_id"
id="project"
name="project"
class="form-control"
required
ng-options="p.id as p.name for p in projects"/>
</div>
</div>
<div class="row">
<div class="col-sm-offset-2 col-sm-10">
<button type="submit" class="btn btn-default"
ng-click="addTask()"
ng-disabled="!taskForm.$valid">
Add task
</button>
</div>
</div>
</form>
</div>
</div>
</div>
<div class="row">
<div class="col-sm-12">
<table class="table table-striped">
<thead>
<tr>
<th class="col-sm-1">
<small>ID</small>
</th>
<th class="col-sm-6">
<small>Title</small>
</th>
<th class="col-sm-3">
<small>Project</small>
</th>
<th class="col-sm-2">
<small>Status</small>
</th>
</tr>
</thead>
<tbody>
<tr ng-repeat="task in tasks"
ng-controller="StoryTaskListItemController">
<td>
<p><strong>
<a href="">{{task.id}}</a>
</strong></p>
</td>
<td>
<p><strong>
<a href="">{{task.title}}</a>
</strong></p>
<small class="text-muted">{{task.description}}</small>
</td>
<td>
<p>
<a href="#!/project/{{project.id}}/overview"
ng-show="project">
{{project.name}}
</a>
<span class="text-danger" ng-hide="project">
Project not found
</span>
</p>
</td>
<td>
<p class="text-success">{{task.status}}</p>
</td>
</tr>
</tbody>
</table>
</div>
</div>

View File

@ -0,0 +1,43 @@
angular.module('sb.util').
directive('contenteditable', function () {
'use strict';
return {
restrict: 'A',
require: '?ngModel',
link: function (scope, element, attrs, ngModel) {
// Write data to the model
function read() {
var e = element[0];
var html = e.innerText || '';
ngModel.$setViewValue(html);
// If you copy/paste between contenteditable fields, it
// drags the HTML tags along. Unfortunately not all
// browsers will let us modify the clipboard in flight,
// so we have to selectively rewrite it here. This also
// strips all HTML tags out - which is good for sanitizing,
// but we'll want to add a rich text editor.
if (e.innerHTML !== e.innerText) {
e.innerHTML = e.innerText;
}
}
if (!ngModel) {
return; // do nothing if no ng-model
}
// Specify how UI should be updated
ngModel.$render = function () {
element.html(ngModel.$viewValue || '');
};
// Listen for change events to enable binding
element.on('blur keyup change', function () {
scope.$apply(read);
});
read(); // initialize
}
};
});

View File

@ -0,0 +1,38 @@
/*
* Copyright (c) 2013 Hewlett-Packard Development Company, L.P.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License. You may obtain
* a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*/
/**
* This directive adds the ng-shift-enter directive. It intercepts keystrokes
* and will execute the bound method if that keystroke is the enter key.
*
* @author Michael Krotscheck
*/
angular.module('sb.util').directive('ngShiftEnter', function () {
'use strict';
return function (scope, element, attrs) {
element.bind('keydown keypress', function (event) {
if (event.which === 13 && event.shiftKey) {
scope.$apply(function () {
scope.$eval(attrs.ngShiftEnter);
});
event.preventDefault();
}
});
};
});

View File

@ -17,7 +17,7 @@
/**
* Generic overrides and addons for bootstrap.
*/
h1, h2, h3, h4, h5, h6 {
h1, h2, h3, h4, h5, h6, p {
&.no-margin {
margin: 0px;
@ -157,4 +157,26 @@ table.table.table-outlined {
// to get rid of.
.modal-content .panel {
margin-bottom: 0px;
}
// Added highlighting for enabled conteneditable areas
*[contenteditable=true] {
// Most of this copied from .form-control, removing the font declarations.
padding: @padding-base-vertical @padding-base-horizontal;
color: @input-color;
background-color: @input-bg;
background-image: none;
border: 1px solid @white;
border-radius: @input-border-radius;
&:hover {
border-color: @input-border;
.box-shadow(inset 0 1px 1px rgba(0,0,0,.075));
}
.transition(~"border-color ease-in-out .15s, box-shadow ease-in-out .15s");
// Customize the `:focus` state to imitate native WebKit styles.
.form-control-focus();
}

View File

@ -0,0 +1,49 @@
/*
* Copyright (c) 2014 Hewlett-Packard Development Company, L.P.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License. You may obtain
* a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*/
/**
* Discussion styles.
*/
.discussion {
.discussion-comment-none {
border-top: 1px solid @table-border-color;
border-bottom: 1px solid @table-border-color;
padding: @table-cell-padding;
margin: 0px;
}
.discussion-comment {
* {
margin: 0px;
}
.discussion-comment-author {
border-top: 1px solid @table-border-color;
background-color: @gray-lighter;
padding: @table-condensed-cell-padding;
}
> p {
padding: @table-cell-padding;
}
}
.discussion-comment-form {
&:last-child {
margin-top: 1em;
}
}
}

View File

@ -31,4 +31,5 @@
@import './custom_font_icons.less';
// Module specific styles
@import './body.less';
@import './auth.less';
@import './auth.less';
@import './discussion.less';