diff --git a/bower.json b/bower.json index 7433383b0d..3a1b4d65cc 100644 --- a/bower.json +++ b/bower.json @@ -21,7 +21,6 @@ "angular-sanitize": "~1.2.1", "angular-resource": "~1.2.1", "angular-ui": "~0.4.0", - "angular-ui-utils": "~0.0.4", "angular-bootstrap": "~0.5.0", "angular-ui-router": "ca3b4777a603df8f86cfd653c8f6c38b2ae05d89", "angular-loading-bar": "~0.0.5", @@ -29,7 +28,7 @@ "bootstrap": "v2.3.2", "bootstrap-datepicker": "~1.2.0", "bootstrap-growl": "~1.1.0", - "habitrpg-shared": "git://github.com/HabitRPG/habitrpg-shared.git#develop", + "habitrpg-shared": "git://github.com/HabitRPG/habitrpg-shared.git#checklists", "BrowserQuest": "https://github.com/mozilla/BrowserQuest.git", "github-buttons": "git://github.com/mdo/github-buttons.git", "marked": "~0.2.9", @@ -38,13 +37,14 @@ "js-emoji": "git://github.com/snicker/js-emoji#master", "gemoji": "git://github.com/github/gemoji", "sticky": "*", - "bootstrap-tour": "~0.8.0" + "bootstrap-tour": "~0.8.0", + "angular-ui-utils": "~0.1.0" }, "resolutions": { "jquery": "~2.0.3", "bootstrap": "v2.3.2", "angular": "~1.2.1", - "bootstrap-tour": "~0.8.0" + "angular-ui-utils": "~0.1.0" }, "devDependencies": { "angular-mocks": "~1.2.1" diff --git a/package.json b/package.json index 9e2ac50493..2efeddc301 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "version": "0.0.0-152", "main": "./src/server.js", "dependencies": { - "habitrpg-shared": "git://github.com/HabitRPG/habitrpg-shared#develop", + "habitrpg-shared": "git://github.com/HabitRPG/habitrpg-shared#checklists", "derby-auth": "git://github.com/lefnire/derby-auth#master", "connect-mongo": "*", "passport-facebook": "~1.0.0", diff --git a/public/css/index.styl b/public/css/index.styl index 796dc2e278..7fbb6f7615 100644 --- a/public/css/index.styl +++ b/public/css/index.styl @@ -1,4 +1,5 @@ -@import "nib/vendor" +@import "nib" +//@import "nib/vendor" // Vendor Includes - include first so we can override // Import only styles that do not have urls to images! Include them directly in the page! diff --git a/public/css/tasks.styl b/public/css/tasks.styl index 78a02d007e..c9e96e4359 100644 --- a/public/css/tasks.styl +++ b/public/css/tasks.styl @@ -407,4 +407,34 @@ form color: #333 &:hover border-top: 0 - margin-top: 2px \ No newline at end of file + margin-top: 2px + +// Checklists +// -------- +.checklist-form + /* Make an input blend into its parent */ + input.inline-edit + reset-box-model() + reset-font() + line-height: inherit + text-align: inherit + background-color:inherit + box-shadow: none + + /* Add interaction cues on hover and focus */ + input.inline-edit:hover,input.inline-edit:focus + background-color: #FFD + transition: background-color 0.5s + + .checklist-icons + opacity:0 + + li + min-height: 20px + padding-left: 10px + margin-bottom: 5px + + [type="checkbox"] + margin: 0px 10px 3px 0px + li:hover .checklist-icons + opacity:1 diff --git a/public/js/controllers/tasksCtrl.js b/public/js/controllers/tasksCtrl.js index f2d82ff08b..8a1ddfcdb7 100644 --- a/public/js/controllers/tasksCtrl.js +++ b/public/js/controllers/tasksCtrl.js @@ -1,7 +1,7 @@ "use strict"; -habitrpg.controller("TasksCtrl", ['$scope', '$rootScope', '$location', 'User','Notification', '$http', 'API_URL', - function($scope, $rootScope, $location, User, Notification, $http, API_URL) { +habitrpg.controller("TasksCtrl", ['$scope', '$rootScope', '$location', 'User','Notification', '$http', 'API_URL', '$timeout', + function($scope, $rootScope, $location, User, Notification, $http, API_URL, $timeout) { $scope.obj = User.user; // used for task-lists $scope.user = User.user; @@ -42,9 +42,11 @@ habitrpg.controller("TasksCtrl", ['$scope', '$rootScope', '$location', 'User','N User.user.ops.deleteTask({params:{id:list[$index].id}}) }; - $scope.saveTask = function(task) { + $scope.saveTask = function(task, stayOpen) { + if (task.checklist && task.checklist[0]) + task.checklist = _.filter(task.checklist,function(i){return !!i.text}); User.user.ops.updateTask({params:{id:task.id},body:task}); - task._editing = false; + if (!stayOpen) task._editing = false; }; /** @@ -68,6 +70,52 @@ habitrpg.controller("TasksCtrl", ['$scope', '$rootScope', '$location', 'User','N }); }; + /* + ------------------------ + To-Dos + ------------------------ + */ + $scope._today = moment().add('days',1); + + /* + ------------------------ + Checklists + ------------------------ + */ + function focusChecklist(task,index) { + window.setTimeout(function(){ + $('#task-'+task.id+' .checklist-form .inline-edit')[index].focus(); + }); + } + $scope.addChecklist = function(task) { + task.checklist = [{completed:false,text:""}]; + focusChecklist(task,0); + } + $scope.addChecklistItem = function(task,$event,$index) { + if ($index == task.checklist.length-1){ + User.user.ops.updateTask({params:{id:task.id},body:task}); // don't preen the new empty item + task.checklist.push({completed:false,text:''}); + focusChecklist(task,task.checklist.length-1); + } else { + $scope.saveTask(task,true); + focusChecklist(task,$index+1); + } + } + $scope.removeChecklistItem = function(task,$index,force){ + var backspaced = !force && !task.checklist[$index].text + if (force || backspaced) { + task.checklist.splice($index,1); + $scope.saveTask(task,true); + if (backspaced) focusChecklist(task,$index-1); + } + } + $scope.navigateChecklist = function(task,$index,$event){ + focusChecklist(task, $event.keyCode == '40' ? $index+1 : $index-1); + } + $scope.checklistCompletion = function(checklist){ + return _.reduce(checklist,function(m,i){return m+(i.completed ? 1 : 0);},0) + } + /* ------------------------ Items diff --git a/public/manifest.json b/public/manifest.json index dee320860c..6fc51a7f19 100644 --- a/public/manifest.json +++ b/public/manifest.json @@ -11,7 +11,7 @@ "bower_components/angular-ui-router/release/angular-ui-router.js", "bower_components/angular-resource/angular-resource.min.js", "bower_components/angular-ui/build/angular-ui.js", - "bower_components/angular-ui-utils/modules/keypress/keypress.js", + "bower_components/angular-ui-utils/ui-utils.min.js", "bower_components/angular-loading-bar/build/loading-bar.js", "bower_components/Angular-At-Directive/src/at.js", "bower_components/Angular-At-Directive/src/caret.js", diff --git a/src/models/task.js b/src/models/task.js index e160a09971..865c2558a1 100644 --- a/src/models/task.js +++ b/src/models/task.js @@ -39,6 +39,13 @@ var HabitSchema = new Schema( , { _id: false } ); +var checklist = [{ + completed:{type:Boolean,'default':false}, + text: String, + _id:false, + id: {type:String,'default':shared.uuid} +}]; + var DailySchema = new Schema( _.defaults({ type: {type:String, 'default': 'daily'}, @@ -53,6 +60,7 @@ var DailySchema = new Schema( s: {type: Boolean, 'default': true}, su: {type: Boolean, 'default': true} }, + checklist:checklist, streak: {type: Number, 'default': 0} }, TaskSchema) , { _id: false } @@ -62,7 +70,8 @@ var TodoSchema = new Schema( _.defaults({ type: {type:String, 'default': 'todo'}, completed: {type: Boolean, 'default': false}, - date: String // due date for todos // FIXME we're getting parse errors, people have stored as "today" and "3/13". Need to run a migration & put this back to type: Date + date: String, // due date for todos // FIXME we're getting parse errors, people have stored as "today" and "3/13". Need to run a migration & put this back to type: Date + checklist:checklist }, TaskSchema) , { _id: false } ); diff --git a/src/models/user.js b/src/models/user.js index 1c32342c1c..8726e31e81 100644 --- a/src/models/user.js +++ b/src/models/user.js @@ -231,7 +231,8 @@ var UserSchema = new Schema({ costume: Boolean, sleep: {type: Boolean, 'default': false}, stickyHeader: {type: Boolean, 'default': true}, - disableClasses: {type: Boolean, 'default': false} + disableClasses: {type: Boolean, 'default': false}, + inlineChecklists: {type: Boolean, 'default': true} }, profile: { blurb: String, diff --git a/views/options/settings.jade b/views/options/settings.jade index b74cf26481..3375bf5445 100644 --- a/views/options/settings.jade +++ b/views/options/settings.jade @@ -38,6 +38,10 @@ script(type='text/ng-template', id='partials/options.settings.settings.html') input(type='checkbox', ng-click='toggleStickyHeader()', ng-checked='user.preferences.stickyHeader!==false') | Sticky Header i.icon-question-sign(popover-trigger='mouseenter', popover-placement='right', popover='Affix the header to the top of the screen. Unchecked means it scrolls out of view.') + label.checkbox + input(type='checkbox', ng-click='set({"preferences.inlineChecklists":user.preferences.inlineChecklists?false:true})', ng-checked='user.preferences.inlineChecklists!==false') + | Inline Checklists + i.icon-question-sign(popover-trigger='mouseenter', popover-placement='right', popover='Show Daily & To-Do Checklists on the task at all times, instead of only when editing.') button.btn(ng-click='showTour()', popover-placement='right', popover-trigger='mouseenter', popover='Restart the introductory tour from when you first joined HabitRPG.') Show Tour br button.btn(ng-click='showBailey()', popover-trigger='mouseenter', popover-placement='right', popover='Bring Bailey the Town Crier out of hiding so you can review past news.') Show Bailey diff --git a/views/shared/tasks/task.jade b/views/shared/tasks/task.jade index 2a94557ce1..753becb957 100644 --- a/views/shared/tasks/task.jade +++ b/views/shared/tasks/task.jade @@ -1,10 +1,10 @@ -li(bindonce='list', ng-repeat='task in obj[list.type+"s"]', class='task {{Shared.taskClasses(task, user.filters, user.preferences.dayStart, user.lastCron, list.showCompleted, main)}}', ng-click='spell && castEnd(task, "task", $event)', ng-class='{"cast-target":spell}', popover-trigger='mouseenter', popover-placement='top', popover='{{task.notes}}') +li(bindonce='list', bo-id='"task-"+task.id', ng-repeat='task in obj[list.type+"s"]', class='task {{Shared.taskClasses(task, user.filters, user.preferences.dayStart, user.lastCron, list.showCompleted, main)}}', ng-click='spell && castEnd(task, "task", $event)', ng-class='{"cast-target":spell}', popover-trigger='mouseenter', popover-placement='top', popover='{{task.notes}}') // right-hand side control buttons .task-meta-controls // Due Date span.task-date(ng-if='task.type=="todo" && task.date') - | {{task.date}} + span(ng-class='{"label label-important":moment(task.date).isBefore(_today)}') {{moment(task.date).format('MM/DD')}} // Streak span(ng-show='task.streak') {{task.streak}} span(tooltip='Streak Counter') @@ -12,6 +12,9 @@ li(bindonce='list', ng-repeat='task in obj[list.type+"s"]', class='task {{Shared // Icons only available if you own the tasks (aka, hidden from challenge stats) span(ng-if='!obj._locked') + span.badge(ng-if='task.checklist[0]', ng-class='{"badge-success":checklistCompletion(task.checklist) == task.checklist.length}') + {{checklistCompletion(task.checklist)}}/{{task.checklist.length}} + i.icon-tags(tooltip='{{appliedTags(user.tags, task.tags)}}', ng-hide='Shared.noTags(task.tags)') // edit a(ng-hide='task._editing', ng-click='editTask(task)', tooltip='Edit') @@ -60,10 +63,16 @@ li(bindonce='list', ng-repeat='task in obj[list.type+"s"]', class='task {{Shared input.visuallyhidden.focusable(ng-if='!$state.includes("tasks")', id='box-{{obj._id}}_{{task.id}}', type='checkbox') label(for='box-{{obj._id}}_{{task.id}}') // main content - p.task-text + div.task-text(ng-dblclick='editTask(task)') markdown(ng-model='task.text',target='_blank') //-| {{task.text}} + div(ng-if='task.checklist && !$state.includes("options.social.challenges") && user.preferences.inlineChecklists!==false && !task._editing') + fieldset.option-group + label.checkbox(ng-repeat='item in task.checklist') + input(type='checkbox',ng-model='item.completed',ng-change='saveTask(task,true)') + | {{item.text}} + // edit/options dialog div(ng-if='task._editing') .task-options @@ -96,6 +105,23 @@ li(bindonce='list', ng-repeat='task in obj[list.type+"s"]', class='task {{Shared |  |  a(ng-click="unlink(task, 'remove-all')") Remove Them + // Checklists + a.btn.btn-small(ng-if='!task.checklist[0] && (task.type=="daily" || task.type=="todo")',ng-click='addChecklist(task)') + i.icon-tasks + | Add Checklist + form.checklist-form(ng-if='task.checklist') + fieldset.option-group.task-checklist(ng-if='!$state.includes("options.social.challenges")') + legend.option-title + | Checklist  + i.icon.icon-question-sign(popover='For Dailies, partially-completed Dailies will cut damage (eg, a 4-item checklist with 3 completed, your Daily will hurt you 25% of the damage). For To-Dos, when completed, your points will be multiplied by the number of checklist-items (eg, a 4-item To-Do will be 4x the Exp & GP).',popover-trigger='mouseenter',popover-placement='bottom') + ul.unstyled + li(ng-repeat='item in task.checklist') + a.pull-right.checklist-icons(ng-click='removeChecklistItem(task,$index,true)') + i.icon-trash(tooltip='Delete') + input(type='checkbox',ng-model='item.completed',ng-change='saveTask(task,true)') + input.inline-edit(type='text',ng-model='item.text',ui-keyup="{13:'addChecklistItem(task,$event,$index)','8 49':'removeChecklistItem(task,$index)','38 40':'navigateChecklist(task,$index,$event)'}")//-,ng-blur='saveTask(task,true)') + + form(ng-submit='saveTask(task)') // text & notes fieldset.option-group @@ -138,7 +164,7 @@ li(bindonce='list', ng-repeat='task in obj[list.type+"s"]', class='task {{Shared // if Todos, the due date fieldset.option-group(ng-if='task.type=="todo" && !task.challenge.id') legend.option-title Due Date - input.option-content.datepicker(type='text', data-date-format='mm/dd/yyyy', ng-model='task.date') + input.option-content.datepicker(type='text', datepicker-popup='MM/dd/yyyy', ng-model='task.date') fieldset.option-group(ng-if='!$state.includes("options.social.challenges")') legend.option-title Tags