From 4cde8f6fe2034151a63ad81d40861a5e5cdc5ef7 Mon Sep 17 00:00:00 2001 From: Is Isilon Date: Mon, 1 Feb 2016 16:37:05 +0800 Subject: [PATCH] Added url to sub-lists --- bower.json | 3 + public/index.html | 56 ++++---- public/javascripts/app.js | 2 +- public/javascripts/collections/list.js | 18 ++- public/javascripts/views/list.js | 95 +++++++------- public/javascripts/views/page.js | 119 ++++++++++++++--- public/javascripts/views/task.js | 34 +++-- public/sass/app.scss | 3 + public/stylesheets/app.css | 169 ++++++++++++++++--------- public/templates/task.html | 3 +- 10 files changed, 329 insertions(+), 173 deletions(-) diff --git a/bower.json b/bower.json index 561b7de..eb8d4fa 100644 --- a/bower.json +++ b/bower.json @@ -23,5 +23,8 @@ "normalize-css": "normalize.css#~3.0.3", "backbone.marionette": "~2.4.4", "underscore": "~1.8.3" + }, + "resolutions": { + "jquery": "^1.8.0 || ^2.0.0" } } diff --git a/public/index.html b/public/index.html index 2acc2f4..adb8f6f 100644 --- a/public/index.html +++ b/public/index.html @@ -10,6 +10,8 @@ HackFlowy + + @@ -17,7 +19,11 @@
- +
@@ -28,37 +34,37 @@
+
- Make love. Catch fish +
+ Make love. Catch fish + +
+ + + open source + +
diff --git a/public/javascripts/app.js b/public/javascripts/app.js index 11a1f9b..e54bf07 100644 --- a/public/javascripts/app.js +++ b/public/javascripts/app.js @@ -28,5 +28,5 @@ require([ 'views/page' ], function (App) { - new App(); + pageView = new App(); }); diff --git a/public/javascripts/collections/list.js b/public/javascripts/collections/list.js index 45b0330..0e6cdd6 100644 --- a/public/javascripts/collections/list.js +++ b/public/javascripts/collections/list.js @@ -24,20 +24,30 @@ localforageBackbone this.on('add remove', this.updateModelPriority); }, + /** + * Move a model in the list up. Only a sort event is emitted + * @param {Backbone.Model} model - Model to be moved + */ moveUp: function(model) { // I see move up as the -1 var index = this.indexOf(model); if (index > 0) { - this.remove(model, {silent: true}); // silence this to stop excess event triggers - this.add(model, {at: index-1}); + this.remove(model, {silent: true}); + this.add(model, {at: index-1, silent: true}); } + this.trigger('sort', this, {}); }, + /** + * Move a model in the list up. Only a sort event is emitted + * @param {Backbone.Model} model - Model to be moved + */ moveDown: function(model) { // I see move up as the -1 var index = this.indexOf(model); if (index < this.models.length) { - this.remove(model, {silent: true}); // silence this to stop excess event triggers - this.add(model, {at: index+1}); + this.remove(model, {silent: true}); + this.add(model, {at: index+1, silent: true}); } + this.trigger('sort', this, {}); }, /** Updated priority of each member of list **/ diff --git a/public/javascripts/views/list.js b/public/javascripts/views/list.js index a6f3499..82f4125 100644 --- a/public/javascripts/views/list.js +++ b/public/javascripts/views/list.js @@ -3,7 +3,6 @@ define( 'backbone', 'collections/list', 'views/task', - 'data/demo', 'text!../../templates/task.html', 'marionette' ], @@ -13,7 +12,6 @@ define( Backbone, List, TaskView, - demoData, listTemplate, Marionette ) { @@ -27,59 +25,53 @@ define( template: _.template(listTemplate), events: { - 'click #add': 'addTask' + 'click #add': 'addTask', }, initialize: function () { - var self = this; - - // this wholeCollection holds all items - this.wholeCollection = Tasks = new List(); - //this.collection = new List(); - - // custom events - this.listenTo(this, 'childview:rerender', this.render); - // this.listenTo(this.collection, 'add remove', this.render); - - /** Load demo data **/ - function loadDemoData() { - for (var i = 0; i < demoData.length; i++) { - var task = Tasks.add(demoData[i]); - task.save(); - } - } - - function success(children, data, promise) { - // load demo data if the server returns nothing - var directChildren = children.filter(this.filterDirectChildren); - if (directChildren.length === 0) - loadDemoData(); - this.collection = new List(Tasks.filter(this.filterDirectChildren)); - this.render(); - } - - Tasks.fetch({ - success: success, - error: function () { - - // switch to localforage database if server isn't present and fetch again - // from there - window.hackflowyOffline = true; - $('#header').append('
Running in offline mode, data may be lost
'); - Tasks.fetch({ - success: success, - context: this - }); - }, - context: this - }); - }, + + /** Get root id for the displayed list from the url hash or 0 **/ + // getRootId: function(){ + // var hash = window.location.hash.slice(1); + // return hash ? hash: 0; + // + // }, + + // /** Called when window location hash changes **/ + // changeRootId: function(){ + // this.collection.remove(this.collection.models); + // this.collection.add(pageView.collection.filter(this.filterDirectChildren,this)); + // this.updateBreadCrumbs(); + // }, + + // updateBreadCrumbs: function(){ + // var rootId = this.getRootId(); + // if (rootId){ + // var current = Tasks.get(rootId); + // var breadCrumbs = _.template('<%= content %>')(current.attributes); + // var depth=0; + // while (current.get('parentId') && depth<100){ + // depth++; + // current = Tasks.get(current.get('parentId')); + // breadCrumbs=_.template('<%= content %> > ')(current.attributes)+breadCrumbs; + // } + // if (depth>=100) console.error('Max depth exceeded while making breadCrumbs'); + // breadCrumbs='Home > '+breadCrumbs; + // $('#task-breadcrumbs').append(breadCrumbs); + // + // } + // }, + // Only show direct children - filterDirectChildren: function (child, index, collection) { - return child.get('parentId') === 0; - }, + // filterDirectChildren: function (child, index, collection) { + // var rootId = this.getRootId(); + // if (rootId) + // return child.get('id') == rootId; + // else + // return child.get('parentId') == rootId; + // }, /** This is the root view in the tree **/ getParentView: function () { @@ -88,8 +80,9 @@ define( /** Update parentId when added to collection **/ onAddChild: function(childView){ - if (childView.model.get('parentId')!==0) - childView.model.save({parentId: 0}); + var rootId = pageView.getRootId(); + if (childView.model.get('parentId')!=rootId && childView.model==rootId) + childView.model.save({parentId: rootId}); }, }); diff --git a/public/javascripts/views/page.js b/public/javascripts/views/page.js index b3b7c8e..3e9a0b5 100644 --- a/public/javascripts/views/page.js +++ b/public/javascripts/views/page.js @@ -1,41 +1,130 @@ define( ['jquery', 'backbone', +'marionette', 'views/list', -'models/task' +'data/demo', +'models/task', +'collections/list' ], function( $, Backbone, +Marionette, ListView, -Task +demoData, +Task, +List ) { - var PageView = Backbone.View.extend({ + var PageView = Marionette.View.extend({ - el: $("#main"), + el: $("#hackflowy"), events: { - 'keypress #newTask': 'createNewTask', - 'blur #newTask': 'createNewTask' }, initialize: function() { - listView = this.listView = new ListView(); - this.input = $('#newTask'); + + // this wholeCollection holds all items + this.collection = new List(); + + + function success(children, data, promise) { + // load demo data if the server returns nothing + var directChildren = children.filter(this.filterDirectChildren,this); + if (directChildren.length === 0){ + for (var i = 0; i < demoData.length; i++) { + var task = this.collection.add(demoData[i]); + task.save(); + } + } + this.listView.collection.add(children.filter(this.filterDirectChildren,this)); + this.listView.render(); + this.updateBreadCrumbs(); + } + + this.collection.fetch({ + success: success, + error: function () { + // switch to localforage database if server isn't present and fetch again + // from there + window.hackflowyOffline = true; + this.$('#header-alerts').append('
Running in offline mode, data may be lost
'); + this.collection.fetch({ + success: success, + context: this + }); + }, + context: this + }); + + var rootItems = new List(this.collection.filter(this.filterDirectChildren,this)); + this.listView = new ListView({collection: rootItems}); + + + // change root item when the url hash changes + // HACK should ideally use http://stackoverflow.com/a/19114496/221742 + $(window).on("hashchange", this.changeRootId.bind(this)); // stop browser going back a page when jamming backspace - window.addEventListener('keydown',function(e){if(e.keyIdentifier=='U+0008'||e.keyIdentifier=='Backspace'){if(e.target==document.body){e.preventDefault();}}},true); + $(window).on('keydown',function(e){if(e.keyIdentifier=='U+0008'||e.keyIdentifier=='Backspace'){if(e.target==document.body){e.preventDefault();}}}); + }, - createNewTask: function(e) { - if (e.keyCode != 13) return; - if (!this.input.val().trim()) return; - this.listView.collection.add(new Task({content: this.input.val().trim() })); - this.input.val(''); - } + /** remove non backbone listener on delete **/ + onDestroy: function() { + $(window).off("haschange",this.changeRootId); + $(window).off('keydown',function(e){if(e.keyIdentifier=='U+0008'||e.keyIdentifier=='Backspace'){if(e.target==document.body){e.preventDefault();}}}); + }, + + /** Get root id for the displayed list from the url hash or 0 **/ + getRootId: function(){ + var hash = window.location.hash.slice(1); + if (hash && pageView.collection.get(hash)) + return hash; + else if (hash) + window.location.hash=''; + return 0; + }, + + /** Called when window location hash changes **/ + changeRootId: function(){ + // change the listview children + this.listView.collection.remove(this.collection.models); + this.listView.collection.add(this.collection.filter(this.filterDirectChildren,this)); + this.updateBreadCrumbs(); + }, + + // Only show direct children + filterDirectChildren: function (child, index, collection) { + var rootId = this.getRootId(); + if (rootId) + return child.get('id') == rootId; + else + return child.get('parentId') == rootId; + }, + + updateBreadCrumbs: function(){ + var rootId = this.getRootId(); + if (rootId){ + this.$('#task-breadcrumbs').empty(); + var current = this.collection.get(rootId); + var breadCrumbs = '';//_.template('<%= content %>')(current.attributes); + var depth=0; + while (current.get('parentId') && depth<100){ + depth++; + current = this.collection.get(current.get('parentId')); + breadCrumbs=_.template('<%= content %> > ')(current.attributes)+breadCrumbs; + } + if (depth>=100) console.error('Max depth exceeded while making breadCrumbs'); + breadCrumbs='Home > '+breadCrumbs; + this.$('#task-breadcrumbs').append(breadCrumbs); + + } + }, }); diff --git a/public/javascripts/views/task.js b/public/javascripts/views/task.js index 68161bf..a08689e 100644 --- a/public/javascripts/views/task.js +++ b/public/javascripts/views/task.js @@ -22,7 +22,8 @@ define( template: _.template(taskTemplate), //'#task-view-template', tagName: 'ul', - className: "shift", + className: "task-view", + viewComparator: List.prototype.comporator, childView: TaskView, childViewContainer: '.children', childViewOptions: { @@ -50,11 +51,14 @@ define( 'click .complete:first': 'markComplete', 'click .uncomplete:first': 'unmarkComlete', 'click .note:first': 'addNote', - 'click .mouse-tip:first': 'foldChildren', + 'click .fold-button:first': 'foldChildren', + // custom events + 'focus': 'this.model.focusOnView', }, - collectionEvents: {}, + collectionEvents: { + }, childEvents: {}, @@ -69,19 +73,23 @@ define( // backlink this.model.view = this; - var children = Tasks.filter( + var children = pageView.collection.filter( function (child, index, collection) { return child.get('parentId') === this.model.id; }, this); this.collection = new List(children); + // there is probobly a better way to do this + if (this.isEmpty()){ + this.$el.addClass('empty'); + } + // events this.listenTo(this.model, 'change', this.render); this.listenTo(this.model, 'destroy', this.remove); // refresh ui hashes after children are rendered this.listenTo(this, 'render:collection', this.bindUIElements); // custom event - this.listenTo(this, 'childview:rerender', this.render); this.listenTo(this, 'focus', this.model.focusOnView); // updates from server @@ -113,9 +121,9 @@ define( /** Get the parent view or root view **/ getParentView: function () { - var parent = Tasks.get(this.model.get('parentId')); + var parent = pageView.collection.get(this.model.get('parentId')); if (parent) return parent.view; - else return listView; + else return pageView.listView; }, /** Update parentId when added to collection **/ @@ -126,7 +134,7 @@ define( /** Focus on the next visible element down despite list level **/ focusOnPrev: function () { - var all = listView.$el.find('ul:visible'); + var all = pageView.listView.$el.find('ul:visible'); var prev = $(all[all.index(this.$el) - 1]); if (prev.length){ prev.find('input:first').focus(); @@ -135,7 +143,7 @@ define( }, focusOnNext: function () { - var all = listView.$el.find('ul:visible'); + var all = pageView.listView.$el.find('ul:visible'); var next = $(all[all.index(this.$el) + 1]); if (next) next.find('input:first').focus(); @@ -169,13 +177,11 @@ define( // move down list by swapping priority with next sibling e.preventDefault(); this.getParentView().collection.moveDown(this.model); - this.trigger('rerender'); this.model.focusOnView(); } else if (e.ctrlKey && e.keyCode == constants.UP_ARROW) { // move up the list e.preventDefault(); this.getParentView().collection.moveUp(this.model); - this.trigger('rerender'); this.model.focusOnView(); } else if (e.keyCode == constants.DOWN_ARROW) { this.focusOnNext(); @@ -188,7 +194,7 @@ define( if (parentId===0){ } else if (parentId){ - parentModel = Tasks.get(parentId); + parentModel = pageView.collection.get(parentId); this.getParentView().collection.remove(this.model); var index = parentModel.view.getParentView().collection.indexOf(parentModel); if (index<0) index=this.getParentView().collection.length-1; @@ -203,7 +209,7 @@ define( var prevSibling = this.$el.prev('ul'); if (prevSibling.length) { var prevSibId = prevSibling.find('input:first').data('id'); - var prevSibView =Tasks.get(prevSibId).view; + var prevSibView =pageView.collection.get(prevSibId).view; this.getParentView().collection.remove(this.model); prevSibView.collection.add(this.model); this.model.focusOnView(); @@ -269,7 +275,7 @@ define( var index= this.getParentView().collection.indexOf(this.model); - var task = Tasks.add({parentId: this.model.get('parentId')}); + var task = pageView.collection.add({parentId: this.model.get('parentId')}); task.save(); if (index>=0) this.getParentView().collection.add(task, {at:index+1}); diff --git a/public/sass/app.scss b/public/sass/app.scss index d2f4bec..df57299 100644 --- a/public/sass/app.scss +++ b/public/sass/app.scss @@ -162,6 +162,9 @@ body { li.shift9 { margin-left: 270px; } + ul.shift { + margin-bottom: 0px; + } } } #main > .children { diff --git a/public/stylesheets/app.css b/public/stylesheets/app.css index 5dc4700..351b4ca 100644 --- a/public/stylesheets/app.css +++ b/public/stylesheets/app.css @@ -5681,9 +5681,6 @@ body #hackflowy #main .children li.shift8 { body #hackflowy #main .children li.shift9 { margin-left: 270px; } -body #hackflowy #main .children ul.shift { - margin-bottom: 0px; -} /* line 167, ../sass/app.scss */ body #hackflowy #main > .children { margin-left: 0; @@ -5695,54 +5692,58 @@ body #hackflowy #footer { font-size: 12px; } +body #hackflowy #main .children ul.task-view { + margin-bottom: 0px; + margin-left: 0px; +} .options-div{ display: none; } .options{ - position: absolute; -z-index: 1000; - color: rgb(51, 51, 51); -cursor: auto; -display: none; -font-family: 'Helvetica Neue', Arial, sans-serif; -font-size: 16px; -font-weight: normal; -height: 0px; -line-height: 16px; -width: 675px; + position: absolute; + z-index: 1000; + color: rgb(51, 51, 51); + cursor: auto; + display: none; + font-family: 'Helvetica Neue', Arial, sans-serif; + font-size: 16px; + font-weight: normal; + height: 0px; + line-height: 16px; + width: 675px; } .options a { -display: inline-block; -width: 20px; -height: 20px; -cursor: pointer; + display: inline-block; + width: 20px; + height: 20px; + cursor: pointer; } .options-left { -background: #ddd; -border: 1px solid #bbb; -width: 80px; -min-height: 58px; -padding: 10px 0 5px 10px; -margin-bottom: 10px; -border-radius: 4px; -font-family: 'Helvetica Neue', Arial, Sans-serif; -z-index: 8; -margin-top: 8px; -margin-left: -29px; + background: #ddd; + border: 1px solid #bbb; + width: 80px; + min-height: 58px; + padding: 10px 0 5px 10px; + margin-bottom: 10px; + border-radius: 4px; + font-family: 'Helvetica Neue', Arial, Sans-serif; + z-index: 8; + margin-top: 8px; + margin-left: -29px; } .options-left a { -color: #555; -margin-bottom: 5px; -font-size: 12px; -display: block; -margin-right: 10px; -width: auto; + color: #555; + margin-bottom: 5px; + font-size: 12px; + display: block; + margin-right: 10px; + width: auto; } .options-left a:hover { @@ -5750,42 +5751,86 @@ width: auto; } .options-left > .mouse-tip { -content: ''; -position: absolute; -left: 0; -height: 25px; -width: 90px; -margin-top: -36px; -margin-left: -1%; + content: ''; + position: absolute; + left: 0; + height: 25px; + width: 90px; + margin-top: -36px; + margin-left: -1%; } .options-left > .mouse-tip:before { -content: ''; -position: absolute; -top: 10px; -left: 19%; -margin-left: -15px; -height: 0; -width: 0; -border-bottom: 15px solid #ddd; -border-left: 15px solid transparent; -border-right: 15px solid transparent; + content: ''; + position: absolute; + top: 10px; + left: 19%; + margin-left: -15px; + height: 0; + width: 0; + border-bottom: 15px solid #ddd; + border-left: 15px solid transparent; + border-right: 15px solid transparent; } .mouse-tip:before { -content: ''; -position: absolute; -top: 10px; -left: 50%; -margin-left: -15px; -height: 0; -width: 0; -border-bottom: 15px solid #ddd; -border-left: 15px solid transparent; -border-right: 15px solid transparent; + content: ''; + position: absolute; + top: 10px; + left: 50%; + margin-left: -15px; + height: 0; + width: 0; + border-bottom: 15px solid #ddd; + border-left: 15px solid transparent; + border-right: 15px solid transparent; } +.options-left > .fold-button { + position: absolute; + left: 0; + /*height: 25px;*/ + /*width: 90px;*/ + margin-top: -36px; + margin-left: -3%; + font-size: 1.5em; +} +.options-left > .fold-button:hover { + text-decoration: none; +} + +/* By default show fold button */ +.options-left>.fold-button:before { + content: "-"; +} +/* no fold button on empty nodes */ +.empty .options-left:first-child > .fold-button:before { + content: ''; +} +/* expand button on folded nodes */ +.folded .options-left:first-child>.fold-button:before { + content: "+"; +} .task-completed{ text-decoration: line-through; } + +.subtle-link { + color: rgb(51, 51, 51); +} +.subtle-link:hover { + color: rgb(51, 51, 51); + text-decoration: underline; +} + +#task-breadcrumbs { + font-size: smaller; + +} +#task-breadcrumbs a { + color: rgb(51, 51, 51); +} +#task-breadcrumbs a:hover { + color: rgb(51, 51, 51); +} diff --git a/public/templates/task.html b/public/templates/task.html index 7ddece2..8295f1e 100644 --- a/public/templates/task.html +++ b/public/templates/task.html @@ -4,7 +4,8 @@