├── .babelrc ├── src ├── img │ └── tyto.png ├── style │ ├── modules │ │ ├── select.styl │ │ ├── time.styl │ │ ├── menu.styl │ │ ├── cookies.styl │ │ ├── time_modal.styl │ │ ├── animations.styl │ │ ├── fab.styl │ │ ├── edit.styl │ │ ├── board.styl │ │ ├── column.styl │ │ └── task.styl │ ├── _functions.styl │ ├── base.styl │ ├── layout │ │ └── layout.styl │ ├── _var.styl │ ├── states │ │ └── states.styl │ ├── _typography.styl │ └── theme │ │ └── theme.styl ├── script │ ├── views │ │ ├── root.js │ │ ├── cookie.js │ │ ├── tyto.js │ │ ├── select.js │ │ ├── time.js │ │ ├── menu.js │ │ ├── task.js │ │ ├── column.js │ │ ├── board.js │ │ └── edit.js │ ├── config │ │ └── tyto.js │ ├── models │ │ └── tyto.js │ ├── app.js │ ├── controllers │ │ └── tyto.js │ ├── utils │ │ ├── suggestions.js │ │ └── utils.js │ └── templates │ │ └── templates.js ├── markup │ ├── mixins │ │ ├── timeLabel.pug │ │ ├── menu.pug │ │ └── githubStats.pug │ ├── index.pug │ ├── templates │ │ ├── cookieBanner.pug │ │ ├── filterList.pug │ │ ├── column.pug │ │ ├── menu.pug │ │ ├── select.pug │ │ ├── task.pug │ │ ├── timeModal.pug │ │ ├── board.pug │ │ └── edit.pug │ ├── layout-blocks │ │ └── layout.pug │ └── cookies.pug ├── txt │ └── license.txt └── json │ └── intro.json ├── .gitignore ├── ISSUE_TEMPLATE.md ├── PULL_REQUEST_TEMPLATE.md ├── .travis.yml ├── .editorconfig ├── LICENSE ├── LOG.md ├── gulpfile.js ├── test ├── index.html └── test.js ├── CONTRIBUTING.md ├── package.json ├── gulp-config.js └── README.md /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015", "react"] 3 | } 4 | -------------------------------------------------------------------------------- /src/img/tyto.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jh3y/tyto/HEAD/src/img/tyto.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | public/ 3 | src/vendor/ 4 | src/coffee/templates/ 5 | .publish/ 6 | -------------------------------------------------------------------------------- /ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ### Issue summary 2 | 3 | ### Expected behavior 4 | 5 | ### Browser used 6 | 7 | ### Steps to reproduce 8 | -------------------------------------------------------------------------------- /src/style/modules/select.styl: -------------------------------------------------------------------------------- 1 | .tyto-select 2 | padding 50px 3 | display flex 4 | flex-direction column 5 | align-items center 6 | -------------------------------------------------------------------------------- /src/style/_functions.styl: -------------------------------------------------------------------------------- 1 | remify(size) 2 | val = size / (size * 0 + 1) 3 | ((val) / 16)rem 4 | 5 | font-size(size) 6 | font-size size 7 | val = size / (size * 0 + 1) 8 | font-size ((val) / 16)rem 9 | -------------------------------------------------------------------------------- /PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | Fixes # || Adds new feature X 2 | 3 | #### Changes include 4 | * a 5 | * b 6 | * c 7 | 8 | #### Checks 9 | - [ ] Updated documentation (_if applicable_) 10 | - [ ] Updated versioning 11 | - [ ] Passes tests 12 | -------------------------------------------------------------------------------- /src/script/views/root.js: -------------------------------------------------------------------------------- 1 | const RootLayout = Backbone.Marionette.LayoutView.extend({ 2 | el : '#tyto-app', 3 | regions: { 4 | Menu : '#tyto-menu', 5 | Content: '#tyto-content' 6 | } 7 | }); 8 | 9 | export default RootLayout; 10 | -------------------------------------------------------------------------------- /src/style/modules/time.styl: -------------------------------------------------------------------------------- 1 | /*** 2 | 3 | Time label component 4 | 5 | ***/ 6 | .tyto-time 7 | display flex 8 | justify-content flex-end 9 | align-items center 10 | &__icon 11 | &__hours 12 | &__minutes 13 | font-size 16px 14 | -------------------------------------------------------------------------------- /src/markup/mixins/timeLabel.pug: -------------------------------------------------------------------------------- 1 | mixin timeLabel(className) 2 | .tyto-time(title="Time spent", class!=className) 3 | i.tyto-time__icon.material-icons schedule 4 | span(class!=`${className}__hours tyto-time__hours`) <%= tyto.timeSpent.hours %>h 5 | span(class!=`${className}__minutes tyto-time__minutes`) <%= tyto.timeSpent.minutes %>m 6 | -------------------------------------------------------------------------------- /src/style/base.styl: -------------------------------------------------------------------------------- 1 | * 2 | box-sizing border-box 3 | 4 | body 5 | overflow-y scroll 6 | font-family 'Roboto', 'Helvetica', 'Arial', sans-serif 7 | -webkit-font-smoothing antialiased 8 | text-rendering optimizeLegibility 9 | -moz-osx-font-smoothing grayscale 10 | font-feature-settings 'liga' 11 | 12 | a 13 | color grey 14 | font-weight 800 15 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "4" 4 | sudo: false 5 | install: 6 | - npm install -g mocha-phantomjs 7 | - npm install -g gulp 8 | - npm install 9 | before_script: 10 | - phantomjs --version 11 | - mocha-phantomjs --version 12 | - gulp build:complete 13 | - sleep 5 # give gulp background task some time to execute. 14 | script: 15 | - npm run test 16 | -------------------------------------------------------------------------------- /src/markup/mixins/menu.pug: -------------------------------------------------------------------------------- 1 | mixin actionMenu(id, icon, className, fades, position, title) 2 | button.mdl-button.mdl-js-button.mdl-button--icon(id!=`${id}`, class!=`${className}__menu-btn ${fades}`, title!=`${title}`) 3 | i.material-icons= icon 4 | ul.mdl-menu.mdl-js-menu.mdl-js-ripple-effect(for!=`${id}`, class!=`${className}__menu mdl-menu--${position}`) 5 | if block 6 | block 7 | -------------------------------------------------------------------------------- /src/markup/index.pug: -------------------------------------------------------------------------------- 1 | extends layout-blocks/layout 2 | block content 3 | #tyto-layout.mdl-layout.mdl-js-layout 4 | header#tyto-header.tyto-header.mdl-layout__header.mdl-layout__header--transparent.tx--black 5 | .mdl-layout__header-row 6 | .mdl-layout-spacer 7 | img.tyto-logo(src="img/tyto.png") 8 | #tyto-menu.tyto-menu.mdl-layout__drawer 9 | #tyto-content.tyto-content.mdl-layout__content 10 | -------------------------------------------------------------------------------- /src/markup/templates/cookieBanner.pug: -------------------------------------------------------------------------------- 1 | .tyto-cookies.bg--blue 2 | img(src="img/tyto.png") 3 | p. 4 | tyto uses cookies that enable it to provide functionality and a better user experience. By using tyto and closing this message you agree to the use of cookies. Read more... 5 | button.tyto-cookies__accept.mdl-button.mdl-js-button.mdl-button--raised.mdl-button--accent.mdl-js-ripple-effect Close 6 | -------------------------------------------------------------------------------- /src/script/views/cookie.js: -------------------------------------------------------------------------------- 1 | const CookieView = Backbone.Marionette.ItemView.extend({ 2 | template: function(args) { 3 | return Tyto.TemplateStore.cookieBanner(args); 4 | }, 5 | ui: { 6 | closeBtn: '.tyto-cookies__accept' 7 | }, 8 | events: { 9 | 'click @ui.closeBtn': 'closeBanner' 10 | }, 11 | closeBanner: function() { 12 | window.localStorage.setItem('tyto', true); 13 | Tyto.RootView.removeRegion('Cookie'); 14 | this.destroy(); 15 | } 16 | }); 17 | export default CookieView; 18 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # top-most EditorConfig file 2 | root = true 3 | 4 | # Unix-style newlines with a newline ending every file 5 | [*] 6 | end_of_line = lf 7 | insert_final_newline = true 8 | 9 | # Matches multiple files with brace expansion notation 10 | # Set default charset 11 | [*.{js}] 12 | charset = utf-8 13 | 14 | 15 | [**/*.{js,stylus,jade}] 16 | indent_style = space 17 | indent_size = 2 18 | 19 | # Matches the exact files either package.json or .travis.yml 20 | [{package.json,.travis.yml}] 21 | indent_style = space 22 | indent_size = 2 23 | -------------------------------------------------------------------------------- /src/style/layout/layout.styl: -------------------------------------------------------------------------------- 1 | /*** 2 | LAYOUT SPECIFIC STYLES 3 | 4 | Used for defining positional relationships. 5 | 6 | ***/ 7 | .tyto-content 8 | & > div 9 | position fixed 10 | top 56px 11 | right 0 12 | bottom 0 13 | left 0 14 | // Required for IE EDGE 15 | z-index 2 16 | @media(min-width 852px) 17 | top 64px 18 | 19 | .tyto-header 20 | position fixed 21 | top 0 22 | left 0 23 | right 0 24 | 25 | .tyto-content 26 | overflow visible 27 | 28 | .tyto-header 29 | z-index 1 30 | -------------------------------------------------------------------------------- /src/markup/templates/filterList.pug: -------------------------------------------------------------------------------- 1 | ul.tyto-suggestions__list 2 | |<% if (tyto.models.length > 0) { %> 3 | |<% _.each(tyto.models, function(model) { %> 4 | |<% if (model.attributes.boardId) { %> 5 | li.tyto-suggestions__item(data-type="Tasks", data-model-id!="<%= model.attributes.id %>") <%= model.attributes.title %> 6 | |<% } else { %> 7 | li.tyto-suggestions__item(data-type="Boards", data-model-id!="<%= model.attributes.id %>") <%= model.attributes.title %> 8 | |<% } %> 9 | |<% }) %> 10 | |<% } else { %> 11 | li.tyto-suggestions__item No suggestions available... 12 | |<% } %> 13 | -------------------------------------------------------------------------------- /src/style/modules/menu.styl: -------------------------------------------------------------------------------- 1 | /* 2 | Styling for top level layout__drawer menu. 3 | */ 4 | .tyto-menu 5 | &__title 6 | background-image url('../img/tyto.png') 7 | background-size contain 8 | background-repeat no-repeat 9 | background-position center center 10 | height 100px 11 | position relative 12 | padding 20px 13 | margin 20px 14 | 15 | h1 16 | position absolute 17 | bottom 0 18 | text-align center 19 | 20 | &__actions 21 | list-style none 22 | 23 | input[type=file] 24 | display none 25 | -------------------------------------------------------------------------------- /src/style/modules/cookies.styl: -------------------------------------------------------------------------------- 1 | .tyto-cookies 2 | position fixed 3 | display flex 4 | align-items center 5 | justify-content center 6 | z-index 999 7 | right 0 8 | bottom 0 9 | left 0 10 | padding 20px 11 | & > * 12 | margin 10px 13 | & > img 14 | min-width 50px 15 | 16 | &__content 17 | display flex 18 | flex-direction column 19 | align-items center 20 | padding 50px 21 | img 22 | height 100px 23 | margin 30px 24 | h1 25 | h2 26 | margin 0 27 | p 28 | max-width 500px 29 | -------------------------------------------------------------------------------- /src/style/_var.styl: -------------------------------------------------------------------------------- 1 | red = #f44336 2 | pink = #E91E63 3 | purple = #9C27B0 4 | indigo = #3F51B5 5 | blue = #2196F3 6 | cyan = #00BCD4 7 | teal = #009688 8 | green = #4CAF50 9 | lime = #CDDC39 10 | yellow = #FFEB3B 11 | amber = #FFC107 12 | orange = #FF5722 13 | grey = #9E9E9E 14 | black = #000000 15 | white = #ffffff 16 | 17 | colorPalette = { 18 | red : red 19 | pink : pink 20 | purple: purple 21 | indigo: indigo 22 | blue : blue 23 | cyan : cyan 24 | teal : teal 25 | green : green 26 | lime : lime 27 | yellow: yellow 28 | amber : amber 29 | orange: orange 30 | grey : grey 31 | black : black 32 | white : white 33 | } 34 | -------------------------------------------------------------------------------- /src/markup/templates/column.pug: -------------------------------------------------------------------------------- 1 | include ../mixins/menu.pug 2 | 3 | .tyto-column__content 4 | .tyto-column__actions 5 | i.tyto-column__mover.material-icons.does--fade(title="Move column") open_with 6 | h6.tyto-column__title.input--fade.bg--white(contenteditable="true", title="Column title") <%= tyto.title %> 7 | +actionMenu('<%= tyto.id %>--menu', 'more_vert', 'tyto-column', 'does--fade', 'bottom-right', 'Column options') 8 | li.tyto-column__delete-column.mdl-menu__item(title="Delete column") Delete 9 | li.tyto-column__add-task.mdl-menu__item(title="Add task") Add task 10 | .tyto-column__tasks 11 | .tyto-column__action 12 | i.material-icons.tyto-column__add-task.does--fade(title="Add task") add 13 | -------------------------------------------------------------------------------- /src/markup/mixins/githubStats.pug: -------------------------------------------------------------------------------- 1 | mixin githubStats(userName, repoName) 2 | .github-stats 3 | .github-stats__stars 4 | iframe(src = `http://ghbtns.com/github-btn.html?user=${userName}&repo=${repoName}&type=watch&count=true`, allowtransparency="true", frameborder="0", scrolling="0", width="90px", height="20") 5 | .github-stats__forks 6 | iframe(src = `http://ghbtns.com/github-btn.html?user=${userName}&repo=${repoName}&type=fork&count=true`, allowtransparency="true", frameborder="0", scrolling="0", width="90px", height="20") 7 | .github-stats__user 8 | iframe(src = `http://ghbtns.com/github-btn.html?user=${userName}&repo=${repoName}&type=follow&count=true`, allowtransparency="true", frameborder="0", scrolling="0", width="120px", height="20") 9 | -------------------------------------------------------------------------------- /src/style/states/states.styl: -------------------------------------------------------------------------------- 1 | /*** 2 | 3 | States -- 4 | 5 | Various states for elements. 6 | 7 | Mainly concerned with when the app is showing a particular view and 8 | whether the 'bloom' transition is being displayed. 9 | 10 | ***/ 11 | 12 | .is--showing-edit-view 13 | transition background-color .5s 14 | 15 | .is--showing-bloom 16 | .tyto-header 17 | z-index 99999 18 | 19 | .is--selected 20 | position relative 21 | &:after 22 | display block 23 | content '\2713' 24 | position absolute 25 | top 0 26 | right 2px 27 | 28 | .has--bottom-margin 29 | margin-bottom 30px 30 | 31 | .does--fade 32 | opacity .4 33 | transition opacity .25s 34 | &:hover 35 | opacity 1 36 | 37 | .is--hidden 38 | display none 39 | -------------------------------------------------------------------------------- /src/markup/layout-blocks/layout.pug: -------------------------------------------------------------------------------- 1 | doctype 2 | head 3 | meta(charset="utf-8") 4 | meta(http-equiv="X-UA-Compatible", content="IE=edge") 5 | title tyto: manage and organise things 6 | meta(name="viewport", content="width=device-width, initial-scale=1, user-scalable=0, maximum-scale=1.0") 7 | meta(name="description", content="tyto: manage and organise things") 8 | link(rel="stylesheet", href="css/vendor.css") 9 | link(href="https://fonts.googleapis.com/css?family=Roboto", rel="stylesheet") 10 | link(rel="stylesheet", href="https://storage.googleapis.com/code.getmdl.io/1.0.0/material.yellow-red.min.css") 11 | link(rel="stylesheet", href=`css/${name}.min.css`) 12 | body#tyto-app.tyto-app 13 | block content 14 | script(src="js/vendor.js") 15 | script(src=`js/${name}.js`) 16 | -------------------------------------------------------------------------------- /src/markup/templates/menu.pug: -------------------------------------------------------------------------------- 1 | include ../mixins/githubStats.pug 2 | 3 | .mdl-layout-title 4 | .tyto-menu__title 5 | h1 tyto 6 | ul.tyto-menu__actions 7 | li 8 | button.tyto-menu__add-board.mdl-button.mdl-js-button.mdl-js-ripple-effect Add new board 9 | li 10 | button#import-data.tyto-menu__import.mdl-button.mdl-js-button.mdl-js-ripple-effect Import data 11 | li 12 | button#load-data.tyto-menu__load.mdl-button.mdl-js-button.mdl-js-ripple-effect Load data 13 | li 14 | button.tyto-menu__export.mdl-button.mdl-js-button.mdl-js-ripple-effect Export data 15 | li 16 | button.tyto-menu__delete-save.mdl-button.mdl-js-button.mdl-js-ripple-effect Delete data 17 | a.tyto-menu__exporter 18 | input.tyto-menu__importer(type="file") 19 | li 20 | +githubStats('jh3y', 'tyto') 21 | -------------------------------------------------------------------------------- /src/script/views/tyto.js: -------------------------------------------------------------------------------- 1 | import TaskView from './task'; 2 | import BoardView from './board'; 3 | import ColumnView from './column'; 4 | import EditView from './edit'; 5 | import RootView from './root'; 6 | import MenuView from './menu'; 7 | import SelectView from './select'; 8 | import CookieBannerView from './cookie'; 9 | import TimeModalView from './time'; 10 | 11 | const Views = function(Views, App, Backbone) { 12 | Views.Root = RootView; 13 | Views.Task = TaskView; 14 | Views.Column = ColumnView; 15 | Views.Board = BoardView; 16 | Views.Edit = EditView; 17 | Views.Menu = MenuView; 18 | Views.Select = SelectView; 19 | Views.CookieBanner = CookieBannerView; 20 | Views.TimeModal = TimeModalView; 21 | }; 22 | 23 | export default Views; 24 | -------------------------------------------------------------------------------- /src/script/config/tyto.js: -------------------------------------------------------------------------------- 1 | const App = Marionette.Application.extend({ 2 | navigate: (route, opts) => { 3 | Backbone.history.navigate(route, opts); 4 | }, 5 | setRootLayout: () => { 6 | Tyto.RootView = new Tyto.Views.Root(); 7 | }, 8 | NAVIGATION_DURATION: 500, 9 | TASK_COLORS : [ 10 | 'yellow', 11 | 'red', 12 | 'blue', 13 | 'indigo', 14 | 'green', 15 | 'purple', 16 | 'orange', 17 | 'pink' 18 | ], 19 | DEFAULT_TASK_COLOR : 'yellow', 20 | ANIMATION_EVENT : 'animationend webkitAnimationEnd oAnimationEnd', 21 | INTRO_JSON_SRC : 'js/intro.json', 22 | LOADING_CLASS : 'is--loading', 23 | SELECTED_CLASS : 'is--selected', 24 | CONFIRM_MESSAGE : '[tyto] are you sure you wish to delete this item?' 25 | }); 26 | export default App; 27 | -------------------------------------------------------------------------------- /src/markup/templates/select.pug: -------------------------------------------------------------------------------- 1 | |<% if (!tyto.items || tyto.items.length == 0) { %> 2 | p 3 | span To start, 4 | button.tyto-select__add-board.mdl-button.mdl-js-button.mdl-button--raised.mdl-js-ripple-effect Add a board 5 | p 6 | span or 7 | button.tyto-select__load-intro-board.mdl-button.mdl-js-button.mdl-button--raised.mdl-js-ripple-effect Load the intro board 8 | |<% } else {%> 9 | p 10 | span To start, select one of your boards. 11 | .selector 12 | select.tyto-select__board-selector 13 | option --select a board-- 14 | |<% _.each(tyto.items, function(i){ %> 15 | option(value!="<%= i.id %>") <%= i.title %> 16 | |<% }) %> 17 | p 18 | span Alternatively, 19 | button.tyto-select__add-board.mdl-button.mdl-js-button.mdl-button--raised.mdl-js-ripple-effect Add a board 20 | |<% } %> 21 | -------------------------------------------------------------------------------- /src/style/modules/time_modal.styl: -------------------------------------------------------------------------------- 1 | .tyto-time-modal 2 | max-width 100% 3 | flex 0 1 auto 4 | animation-duration .25s 5 | animation-name create 6 | 7 | &__wrapper 8 | position fixed 9 | top 0 10 | right 0 11 | bottom 0 12 | left 0 13 | background-color rgba(0,0,0,0.5) 14 | z-index 99999 15 | display flex 16 | flex-direction column 17 | align-items center 18 | justify-content center 19 | animation-duration .25s 20 | animation-name fade__in 21 | 22 | &__content 23 | width auto 24 | height auto 25 | min-height 0 26 | margin 10px 27 | 28 | &__content-title 29 | justify-content center 30 | padding-bottom 0 31 | 32 | &__content-text 33 | width 100% 34 | padding-top 0 35 | -------------------------------------------------------------------------------- /src/markup/templates/task.pug: -------------------------------------------------------------------------------- 1 | include ../mixins/menu.pug 2 | include ../mixins/timeLabel.pug 3 | .tyto-task__content 4 | .tyto-task__header.tx--center 5 | i.material-icons.tyto-task__mover.does--fade(title="Move task") open_with 6 | h2.tyto-task__title(contenteditable="true", title="Task title") <%= tyto.title %> 7 | +actionMenu('<%= tyto.id %>--menu', 'more_vert', 'tyto-task', 'does--fade', 'bottom-right', 'Task options') 8 | li.mdl-menu__item.tyto-task__delete-task(title="Delete task") Delete 9 | li.mdl-menu__item.tyto-task__edit-task(title="Edit task") Edit 10 | li.mdl-menu__item.tyto-task__track-task(title="Track task time") Track 11 | .mdl-card__supporting-text.tyto-task__description(title="Task description") <%= tyto.description %> 12 | .tyto-task__edit-wrapper 13 | textarea.tyto-task__description-edit.is--hidden 14 | .tyto-task__suggestions.tyto-suggestions__container.mdl-shadow--2dp.is--hidden 15 | |<% var hidden = (tyto.timeSpent.hours > 0 || tyto.timeSpent.minutes > 0) ? '': 'is--hidden'; %> 16 | +timeLabel('tyto-task__time') 17 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | /* 2 | tyto - http://jh3y.github.io/tyto 3 | Licensed under the MIT license 4 | 5 | jh3y (c) 2018 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 8 | 9 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 10 | 11 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 12 | */ 13 | -------------------------------------------------------------------------------- /src/markup/cookies.pug: -------------------------------------------------------------------------------- 1 | doctype 2 | head 3 | meta(charset="utf-8") 4 | meta(http-equiv="X-UA-Compatible", content="IE=edge") 5 | title tyto: manage and organise. 6 | meta(name="description", content="") 7 | meta(name="viewport", content="width=device-width, initial-scale=1") 8 | link(rel='stylesheet', href="css/vendor.min.css") 9 | link(rel='stylesheet', href='css/tyto.min.css') 10 | body 11 | .tyto-cookies__content 12 | img(src="img/tyto.png") 13 | h1 14 | a(href="index.html") tyto 15 | h2 tyto's use of cookies 16 | p. 17 | tyto may with your consent use cookies in order to provide you a better user experience. These cookies enable tyto to personalise the site for your use by remembering your use of tyto and storing this data in your browser locally to your computer(never online). 18 | p. 19 | Referring to the ICC UK Cookie guide, tyto uses category 3 cookies. 20 | p. 21 | These are functionality cookies that allow tyto to remember your use. The cookie stored in your browser by tyto can never personally identify you nor can it track you or your browsing activity. 22 | h3 Happy Tytoing! 23 | -------------------------------------------------------------------------------- /src/txt/license.txt: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | tyto - http://jh3y.github.io/tyto 4 | Licensed under the MIT license 5 | 6 | jh3y (c) 2015 7 | 8 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 9 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 10 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 11 | 12 | */ 13 | -------------------------------------------------------------------------------- /src/markup/templates/timeModal.pug: -------------------------------------------------------------------------------- 1 | .tyto-time-modal__content.mdl-card.mdl-shadow--4dp 2 | .tyto-time-modal__content-title.mdl-card__title 3 | h2.mdl-card__title-text <%= tyto.title %> 4 | .tyto-time-modal__content-text.tx--center.mdl-card__supporting-text 5 | p.tyto-time-modal__content-description 6 | h1.tyto-time-modal__timer-lbl 7 | span.tyto-time-modal__timer-lbl-hours 8 | span : 9 | span.tyto-time-modal__timer-lbl-minutes 10 | span : 11 | span.tyto-time-modal__timer-lbl-seconds 12 | .tyto-time-modal__actions.mdl-card__actions.mdl-card--border.tx--center 13 | button.tyto-time-modal__timer-reset.mdl-button.mdl-js-button.mdl-button--icon.mdl-button--accent.mdl-js-ripple-effect(title="Reset time") 14 | i.material-icons restore 15 | button.tyto-time-modal__timer.mdl-button.mdl-js-button.mdl-button--icon.mdl-button--accent.mdl-js-ripple-effect(title="Stop/Start tracking") 16 | i.tyto-time-modal__timer-icon.material-icons play_arrow 17 | button.tyto-time-modal__close.mdl-button.mdl-js-button.mdl-button--icon.mdl-button--accent.mdl-js-ripple-effect(title="Exit tracking") 18 | i.material-icons clear 19 | -------------------------------------------------------------------------------- /LOG.md: -------------------------------------------------------------------------------- 1 | Log ![alt tag](https://raw.github.com/jh3y/tyto/master/src/img/tyto.png) 2 | === 3 | * __11/12/13__: Posted to HN, really great and very appreciated feedback from a large group of people 4 | * __14/02/14__: V1.1.0 5 | * __16/02/14__: V1.2.0. jQuery UI implementation for sorting columns and items 6 | * __23/05/14__: V1.4.0. Introduction of Grunt and Bower 7 | * __01/08/15__: V2.0.0. Complete overhaul of design and functionality. Move to MV* framework and introduction of minimal UI using MDL 8 | * __02/10/15__: Documentation reduction 9 | * __05/10/15__: Add markdown support + bug fixes 10 | * __17/10/15__: Implement dynamic linking between entities via Markdown `#` 11 | * __16/09/16__: V3.0.0 Removal of CoffeeScript in favor of Babel ES6 + various bug fixes/improvements 12 | * __06/12/16__: V3.0.1 Removal of Bower in favor of just npm use with Yarn. Fix issues #60 and #61 13 | * __17/07/2017__: V3.0.2 Update `yarn.lock` and deps to use `jquery/jquery-ui`. Update vendor scripts required. Add `package-lock.json` 14 | * __11/10/2017__: V3.0.3 Update `mocha-phantomjs` and add styling rules for embedded images #67 15 | * __04/01/2018__: V3.0.4 Update `marked` dependency as triggering security warning from Github 16 | -------------------------------------------------------------------------------- /src/style/_typography.styl: -------------------------------------------------------------------------------- 1 | /*** 2 | Material Icons integration. 3 | ***/ 4 | @font-face 5 | font-family 'Material Icons' 6 | font-style normal 7 | font-weight 400 8 | src url('../fonts/MaterialIcons-Regular.eot'); 9 | src local('Material Icons'), 10 | local('MaterialIcons-Regular'), 11 | url('../fonts/MaterialIcons-Regular.woff2') format('woff2'), 12 | url('../fonts/MaterialIcons-Regular.woff') format('woff'), 13 | url('../fonts/MaterialIcons-Regular.ttf') format('truetype'); 14 | 15 | .material-icons 16 | font-family 'Material Icons' 17 | font-weight normal 18 | font-style normal 19 | font-size 24px /* Preferred icon size */ 20 | display inline-block 21 | width 1em 22 | height 1em 23 | line-height 1 24 | text-transform none 25 | letter-spacing normal 26 | word-wrap normal 27 | 28 | /* Support for all WebKit browsers. */ 29 | -webkit-font-smoothing antialiased 30 | /* Support for Safari and Chrome. */ 31 | text-rendering optimizeLegibility 32 | 33 | /* Support for Firefox. */ 34 | -moz-osx-font-smoothing grayscale 35 | 36 | /* Support for IE. */ 37 | font-feature-settings 'liga' 38 | -------------------------------------------------------------------------------- /src/script/models/tyto.js: -------------------------------------------------------------------------------- 1 | const Models = function(Models, App, Backbone) { 2 | Models.Board = Backbone.Model.extend({ 3 | defaults: { 4 | title: 'New Board' 5 | } 6 | }); 7 | Models.BoardCollection = Backbone.Collection.extend({ 8 | localStorage: new Backbone.LocalStorage('tyto--board'), 9 | model : Models.Board 10 | }); 11 | Models.Column = Backbone.Model.extend({ 12 | defaults: { 13 | title : 'New Column', 14 | ordinal: 1 15 | }, 16 | localStorage: new Backbone.LocalStorage('tyto--column') 17 | }); 18 | Models.ColumnCollection = Backbone.Collection.extend({ 19 | model : Models.Column, 20 | localStorage: new Backbone.LocalStorage('tyto--column') 21 | }); 22 | Models.Task = Backbone.Model.extend({ 23 | defaults: { 24 | title : 'New Todo', 25 | description: 'Making this work!', 26 | color : 'yellow', 27 | timeSpent : { 28 | hours : 0, 29 | minutes: 0, 30 | seconds: 0 31 | } 32 | }, 33 | localStorage: new Backbone.LocalStorage('tyto--task') 34 | }); 35 | Models.TaskCollection = Backbone.Collection.extend({ 36 | localStorage: new Backbone.LocalStorage('tyto--task'), 37 | model : Models.Task 38 | }); 39 | }; 40 | 41 | export default Models; 42 | -------------------------------------------------------------------------------- /src/style/modules/animations.styl: -------------------------------------------------------------------------------- 1 | /*** 2 | 3 | Animations -- 4 | 5 | Set of keyframes used across app. Mainly revolve around animating various 6 | scale sizes for elements. 7 | 8 | ***/ 9 | 10 | /* Give newly created entities an entry animation */ 11 | @keyframes create 12 | from 13 | transform scale(0) 14 | to 15 | transform scale(1) 16 | 17 | /* Animate bloomer for view transition */ 18 | @keyframes bloom 19 | from 20 | transform scale(0) 21 | to 22 | transform scale(50) 23 | 24 | /* Fade in content */ 25 | @keyframes fade__in 26 | from 27 | opacity 0 28 | to 29 | opacity 1 30 | 31 | /* FAB speed dial button animation of options */ 32 | @keyframes fab__enter 33 | from 34 | transform scale(0) 35 | to 36 | transform scale(0.8) 37 | 38 | 39 | /* Used for animating in freshly created columns */ 40 | .tyto-board.is--adding-column 41 | .tyto-column 42 | &:last-of-type 43 | .tyto-column__content 44 | transform-origin center center 45 | animation-duration .5s 46 | animation-name create 47 | 48 | 49 | /* 50 | animates a freshly created task with the spring in effect. 51 | */ 52 | .tyto-column.is--adding-task 53 | .tyto-task 54 | &:last-of-type 55 | transform-origin center center 56 | animation-duration .5s 57 | animation-name create 58 | -------------------------------------------------------------------------------- /src/script/views/select.js: -------------------------------------------------------------------------------- 1 | const SelectView = Backbone.Marionette.ItemView.extend({ 2 | template: function(args) { 3 | return Tyto.TemplateStore.select(args); 4 | }, 5 | tagName: 'div', 6 | className: function() { 7 | return this.domAttributes.VIEW_CLASS; 8 | }, 9 | ui: { 10 | add : '.tyto-select__add-board', 11 | load : '.tyto-select__load-intro-board', 12 | boardSelector: '.tyto-select__board-selector' 13 | }, 14 | events: { 15 | 'click @ui.add' : 'addBoard', 16 | 'change @ui.boardSelector': 'showBoard', 17 | 'click @ui.load' : 'loadIntro' 18 | }, 19 | domAttributes: { 20 | VIEW_CLASS: 'tyto-select' 21 | }, 22 | collectionEvents: { 23 | 'all': 'render' 24 | }, 25 | addBoard: function() { 26 | this.showBoard(Tyto.Boards.create().id); 27 | }, 28 | loadIntro: function() { 29 | const view = this; 30 | let id; 31 | Tyto.RootView.$el.addClass(Tyto.LOADING_CLASS); 32 | $.getJSON(Tyto.INTRO_JSON_SRC, function(d) { 33 | Tyto.RootView.$el.removeClass(Tyto.LOADING_CLASS); 34 | Tyto.Utils.load(d, true, false); 35 | _.forOwn(d, function(val, key) { 36 | if (key.indexOf('tyto--board-') !== -1) { 37 | id = JSON.parse(val).id; 38 | } 39 | }); 40 | view.showBoard(id); 41 | }); 42 | }, 43 | showBoard: function(id) { 44 | if (typeof id !== 'string') { 45 | id = this.ui.boardSelector.val(); 46 | } 47 | Tyto.navigate('board/' + id, { 48 | trigger: true 49 | }); 50 | } 51 | }); 52 | 53 | export default SelectView; 54 | -------------------------------------------------------------------------------- /src/style/modules/fab.styl: -------------------------------------------------------------------------------- 1 | /*** 2 | 3 | Floating action button -- 4 | 5 | Styles the board FAB for adding columns and tasks. Implementation based on the maximum amount of sub options being six as set out in Material Design spec. 6 | 7 | ***/ 8 | 9 | positioning = 20px 10 | transitionD = .1s 11 | 12 | .mdl-button--fab_flinger-container 13 | position fixed 14 | bottom positioning 15 | right positioning 16 | z-index 999 17 | &.is-showing-options 18 | & > button 19 | i 20 | transition transform transitionD linear 21 | transform translate(-12px, -12px) rotate(45deg) 22 | .mdl-button--fab_flinger-options 23 | display flex 24 | flex-direction column-reverse 25 | button 26 | /* 27 | Calculate animation delay of each child by using a loop 28 | through finite set up to max children (6). 29 | */ 30 | for child in (1..6) 31 | &:nth-of-type({child}) 32 | animation-delay (transitionD * child) 33 | display block 34 | animation-name fab__enter 35 | animation-fill-mode forwards 36 | animation-duration transitionD 37 | transform-origin bottom center 38 | .mdl-button--fab_flinger-options 39 | position absolute 40 | bottom 100% 41 | margin-bottom 10px 42 | button 43 | transform scale(0) 44 | display none 45 | 46 | /* 47 | Required for FAB button sub icon styling. 48 | In this case, the additional plus icon for column and task icons. 49 | */ 50 | .mdl-button--fab 51 | i 52 | &.sub 53 | transform scale(0.75) 54 | font-weight 800 55 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | var gulp = require('gulp'), 2 | script = require('./build-tasks/script'), 3 | style = require('./build-tasks/style'), 4 | markup = require('./build-tasks/markup'), 5 | tmpl = require('./build-tasks/tmpl'), 6 | deploy = require('./build-tasks/deploy'), 7 | server = require('./build-tasks/serve'), 8 | assets = require('./build-tasks/assets'); 9 | 10 | gulp.task('serve', ['build:complete'], server.start); 11 | 12 | gulp.task('script:compile', ['tmpl:compile'], script.compile); 13 | gulp.task('script:watch', script.watch); 14 | 15 | gulp.task('style:compile', style.compile); 16 | gulp.task('style:watch', style.watch); 17 | 18 | gulp.task('markup:compile', markup.compile); 19 | gulp.task('markup:watch', markup.watch); 20 | 21 | gulp.task('tmpl:compile', tmpl.compile); 22 | gulp.task('tmpl:watch', tmpl.watch); 23 | 24 | gulp.task('vendor:scripts:publish', assets.scripts); 25 | gulp.task('vendor:fonts:publish', assets.fonts); 26 | gulp.task('vendor:styles:publish', assets.styles); 27 | gulp.task('img:publish', assets.img); 28 | gulp.task('json:publish', assets.json); 29 | 30 | 31 | gulp.task('vendor:publish', [ 32 | 'vendor:scripts:publish', 33 | 'vendor:styles:publish', 34 | 'vendor:fonts:publish', 35 | 'img:publish', 36 | 'json:publish' 37 | ]); 38 | 39 | gulp.task('deploy', ['build:complete'], deploy.deploy); 40 | 41 | gulp.task('build:complete', [ 42 | 'markup:compile', 43 | 'tmpl:compile', 44 | 'script:compile', 45 | 'vendor:publish', 46 | 'style:compile' 47 | ]); 48 | gulp.task('watch', [ 49 | 'markup:watch', 50 | 'tmpl:watch', 51 | 'script:watch', 52 | 'style:watch' 53 | ]); 54 | gulp.task('default', [ 55 | 'serve', 56 | 'watch' 57 | ]); 58 | -------------------------------------------------------------------------------- /test/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | tyto: manage and organise things 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 24 |
25 | 26 | 27 | 31 | 32 | 33 | 34 | 41 | 42 | -------------------------------------------------------------------------------- /src/markup/templates/board.pug: -------------------------------------------------------------------------------- 1 | include ../mixins/menu.pug 2 | 3 | .tyto-board__options 4 | +actionMenu('tyto-board__menu', 'more_vert', 'tyto-board', '', 'bottom-right', 'Board options') 5 | li.tyto-board__wipe-board.mdl-menu__item Wipe board 6 | li.tyto-board__delete-board.mdl-menu__item Delete board 7 | li.tyto-board__email-board.mdl-menu__item Email board 8 | a.tyto-board__emailer(style="display: none;") 9 | 10 | .tyto-board__details 11 | h1.tyto-board__title.bg--white(contenteditable="true") 12 | |<%= tyto.title %> 13 | |<% if (tyto.boards.length > 1) { %> 14 | +actionMenu('tyto-board__selector', 'expand_more', 'tyto-board__selector', '', 'bottom-right', 'Select a board') 15 | |<% _.each(tyto.boards.models, function(i){ %> 16 | |<% if (i.attributes.id != tyto.id) { %> 17 | li.mdl-menu__item.tyto-board__selector-option(title="View board") 18 | a(href!="#board/<%= i.attributes.id %>") <%= i.attributes.title %> 19 | |<% } %> 20 | |<% }) %> 21 | |<% } %> 22 | 23 | .tyto-board__columns 24 | 25 | .tyto-board__actions.mdl-button--fab_flinger-container 26 | button.tyto-board__add-entity.mdl-button.mdl-js-button.mdl-button--fab.mdl-js-ripple-effect.mdl-button--colored 27 | i.material-icons add 28 | .mdl-button--fab_flinger-options 29 | button.tyto-board__super-add.mdl-button.mdl-js-button.mdl-button--fab.mdl-js-ripple-effect.mdl-button--colored(title="Add task") 30 | i.material-icons description 31 | i.material-icons.sub add 32 | button.tyto-board__add-column.mdl-button.mdl-js-button.mdl-button--fab.mdl-js-ripple-effect.mdl-button--colored(title="Add column") 33 | i.material-icons view_column 34 | i.material-icons.sub add 35 | 36 | .tyto-board__bloomer 37 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | Contributing to tyto ![alt tag](https://raw.github.com/jh3y/tyto/master/src/img/tyto.png) 2 | === 3 | Open source is a great thing and I really appreciate that you may be interested in contributing to `tyto`! 4 | 5 | ##Issues 6 | Feel free to submit an issue. However. Be sure to make your contribution clear. 7 | 8 | ###Found a bug? 9 | Be clear about what the issue is that you've found and PLEASE provide clear steps to reproduce it :simple_smile: 10 | 11 | ###Want a new feature? 12 | If you would like to see a new feature in `tyto`, raise an issue for it. Give it an appropriate label and then clearly set out what you'd like to see. If you can, also provide a possible solution for this feature to help get the ball rolling. 13 | 14 | ##Making contributions 15 | For me personally. I prefer to work on forked instances of a repo to ensure I'm not stepping on anyones toes. 16 | 17 | For minor fixes and issues. Develop against develop and submit pull requests to be merged into `develop`. 18 | 19 | For new features and large scale changes, please create a new branch on the repo and work against this. Ensure the branch name is labelled appropriately with the correct issue number. 20 | 21 | The most important thing to me is that people aren't afraid of contributing. If you have any problems or get stuck, feel free to get in touch! 22 | 23 | ###Set up 24 | 1. Fork the repo and clone it. 25 | 26 | git clone https://github.com//tyto.git 27 | 28 | 2. Navigate into the repo and install the dependencies. 29 | 30 | cd tyto 31 | yarn/npm install 32 | 33 | 3. Run gulp to take care of preprocessing and running a local static webserver instance(the project uses BrowserSync). 34 | 35 | gulp 36 | 37 | 4. Develop! 38 | -------------------------------------------------------------------------------- /src/style/modules/edit.styl: -------------------------------------------------------------------------------- 1 | .tyto-edit 2 | & > * 3 | animation-name fade__in 4 | animation-duration .25s 5 | 6 | &__content 7 | position fixed 8 | top 56px 9 | right 0 10 | bottom 0 11 | left 0 12 | padding 20px 13 | overflow visible 14 | @media(min-width 852px) 15 | top 64px 16 | @media(min-width 768px) 17 | width 600px 18 | margin 0 auto 19 | 20 | &__color-select__menu 21 | display flex 22 | flex-wrap wrap 23 | flex-direction row 24 | padding 8px 25 | 26 | &__color-select__menu-option 27 | width 50% 28 | 29 | &__task-title 30 | margin 0 32px 0 0 31 | padding 10px 24px 32 | 33 | &__task-description 34 | padding 24px 35 | width 100% 36 | outline 0 37 | background transparent 38 | border none 39 | min-height 100px 40 | 41 | &__edit-description 42 | outline 0 43 | border none 44 | background transparent 45 | padding 16px 0 46 | width 100% 47 | min-height 100px 48 | max-height 300px 49 | margin 0 16px 50 | font-size 16px 51 | line-height 16px 52 | resize none 53 | 54 | &__nav 55 | &__details-footer 56 | min-height 50px 57 | 58 | &__details-footer 59 | padding 0 8px 60 | opacity 0.4 61 | 62 | &__actions 63 | position absolute 64 | top 20px 65 | right 20px 66 | display flex 67 | flex-direction column 68 | 69 | 70 | &__nav 71 | position fixed 72 | display flex 73 | align-items center 74 | z-index 2 75 | top 0 76 | left 100px 77 | height 56px 78 | @media(min-width 852px) 79 | height 64px 80 | -------------------------------------------------------------------------------- /src/style/modules/board.styl: -------------------------------------------------------------------------------- 1 | /*** 2 | 3 | Board component. 4 | 5 | ***/ 6 | 7 | .tyto-board 8 | display flex 9 | flex-direction column 10 | 11 | &__options 12 | position fixed 13 | display flex 14 | align-items center 15 | z-index 2 16 | top 0 17 | right 10px 18 | height 56px 19 | @media(min-width 852px) 20 | height 64px 21 | 22 | &__columns 23 | display flex 24 | flex-direction row 25 | overflow-x auto 26 | overflow-y hidden 27 | flex 1 1 auto 28 | padding 10px 29 | 30 | 31 | &__details 32 | position fixed 33 | display flex 34 | align-items center 35 | z-index 2 36 | top 0px 37 | left 75px 38 | right 75px 39 | 40 | &__selector__menu-btn 41 | margin-left 6px 42 | 43 | &__selector-option 44 | padding 0 45 | a 46 | padding 0 8px 47 | display block 48 | text-decoration none 49 | 50 | &__title 51 | font-size 24px 52 | height 56px 53 | line-height 56px 54 | margin 0 55 | outline 0 56 | min-width 100px 57 | overflow hidden 58 | white-space nowrap 59 | text-overflow ellipsis 60 | &:active 61 | &:focus 62 | overflow visible 63 | white-space normal 64 | @media(min-width 852px) 65 | line-height 64px 66 | height 64px 67 | 68 | &__bloomer 69 | position fixed 70 | height 100px 71 | width 100px 72 | margin-left -50px 73 | margin-top -50px 74 | border-radius 100% 75 | z-index 9999 76 | display none 77 | animation-fill-mode forwards 78 | animation-timing-function linear 79 | animation-duration .5s 80 | &.is--blooming 81 | display block 82 | animation-name bloom 83 | -------------------------------------------------------------------------------- /src/style/theme/theme.styl: -------------------------------------------------------------------------------- 1 | /*** 2 | 3 | THEME -- 4 | 5 | Contains styling rules for various app wide styles. 6 | 7 | ***/ 8 | 9 | 10 | /* 11 | Generates color palette specific styles for background, border, text and 12 | hover color 13 | */ 14 | for key, value in colorPalette 15 | .bg--{key} 16 | background-color value 17 | .tx--{key} 18 | color value 19 | .bd--{key} 20 | border 4px solid value 21 | .hv--{key} 22 | &:hover 23 | background-color value 24 | /* 25 | NOTE: That for [contenteditable] elements with a background color we wish 26 | to show an accented color on :focus/:active. 27 | */ 28 | for key, value in colorPalette 29 | .bg--{key} [contenteditable] 30 | [contenteditable].bg--{key} 31 | .bg--{key} textarea 32 | transition background .25s 33 | outline 0 34 | &:active 35 | &:focus 36 | background-color darken(value, 15%) 37 | /* 38 | Generates text alignment utility classes. 39 | */ 40 | for dir in left right center 41 | .tx--{dir} 42 | text-align dir 43 | 44 | /* 45 | Position helper 46 | */ 47 | position() 48 | position arguments 49 | z-index 1 unless @z-index 50 | 51 | 52 | /* 53 | tx--black wouldn't work on this set up because it's a generated el. 54 | */ 55 | .mdl-layout__drawer-button 56 | i 57 | color black 58 | 59 | .github-stats 60 | margin-top 50px 61 | 62 | /* 63 | NOTE: MDL Override FIX to stop MDL menu clipped menus blocking UI 64 | interaction. 65 | */ 66 | .tyto-board .mdl-menu__container 67 | height 0px !important 68 | 69 | /*** 70 | Suggestions 71 | ***/ 72 | .tyto-suggestions 73 | &__container 74 | position fixed 75 | background white 76 | border-radius 2px 77 | z-index 99999 78 | &__list 79 | list-style none 80 | padding 0 81 | margin 0 82 | font-size 14px 83 | &__item 84 | padding 4px 26px 85 | cursor pointer 86 | line-height 14px 87 | &:hover 88 | &.is--active 89 | color white 90 | background-color teal 91 | -------------------------------------------------------------------------------- /src/style/modules/column.styl: -------------------------------------------------------------------------------- 1 | /*** 2 | 3 | Columns 4 | 5 | ***/ 6 | /* variables */ 7 | columnWidth = 320px 8 | 9 | .tyto-column 10 | display flex 11 | width columnWidth 12 | max-width columnWidth 13 | flex-direction column 14 | flex 0 0 columnWidth 15 | max-height 100% 16 | 17 | &:hover &__action 18 | min-height 40px 19 | height 40px 20 | 21 | &:hover &__actions 22 | .does--fade 23 | &:hover 24 | opacity 1 25 | opacity 0.4 26 | 27 | &__content 28 | display flex 29 | flex-direction column 30 | flex 1 1 auto 31 | width 100% 32 | max-height 100% 33 | padding-right 10px 34 | overflow auto 35 | 36 | &__mover 37 | position absolute 38 | top 0 39 | left 14px 40 | flex 0 0 auto 41 | cursor move 42 | 43 | &__actions 44 | text-align center 45 | margin 20px 0 46 | flex 0 0 auto 47 | position relative 48 | align-items center 49 | .does--fade 50 | opacity 0 51 | 52 | &__title 53 | max-width 200px 54 | min-width 150px 55 | margin 0 56 | display inline-block 57 | 58 | &__menu-btn 59 | position absolute 60 | width 24px 61 | height 24px 62 | border-radius 50% 63 | min-width 24px 64 | padding 0 65 | right 5px 66 | top 0 67 | 68 | &__tasks 69 | padding 0 10px 70 | flex 1 1 auto 71 | overflow-y auto 72 | overflow-x hidden 73 | 74 | &__placeholder 75 | display inline-block 76 | min-width columnWidth 77 | height 100% 78 | 79 | &__action 80 | height 0 81 | min-height 0 82 | overflow hidden 83 | text-align center 84 | display flex 85 | align-items center 86 | align-content center 87 | transition height .25s linear, min-height .25s linear 88 | i 89 | flex 1 90 | font-size 32px 91 | cursor pointer 92 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tyto", 3 | "version": "3.0.4", 4 | "description": "tyto - manage and organise", 5 | "scripts": { 6 | "test": "./node_modules/mocha-phantomjs/bin/mocha-phantomjs -p ./node_modules/.bin/phantomjs test/index.html" 7 | }, 8 | "repository": { 9 | "type": "git", 10 | "url": "https://github.com/jh3y/tyto" 11 | }, 12 | "author": "jh3y ", 13 | "license": "MIT", 14 | "bugs": { 15 | "url": "https://github.com/jh3y/tyto/issues" 16 | }, 17 | "homepage": "https://github.com/jh3y/tyto", 18 | "devDependencies": { 19 | "minimatch": "^3.0.4", 20 | "tough-cookie": "^2.3.3", 21 | "babel-preset-es2015": "^6.14.0", 22 | "babel-preset-react": "^6.11.1", 23 | "babelify": "^7.3.0", 24 | "backbone": "1.1.2", 25 | "backbone.localstorage": "^1.1.16", 26 | "backbone.marionette": "2.4.1", 27 | "backbone.wreqr": "^1.4.0", 28 | "browser-sync": "^2.6.4", 29 | "browserify": "^10.2.0", 30 | "chai": "^3.5.0", 31 | "gulp": "^3.8.11", 32 | "gulp-autoprefixer": "^2.2.0", 33 | "gulp-concat": "^2.5.2", 34 | "gulp-filter": "^2.0.2", 35 | "gulp-gh-pages": "^0.5.1", 36 | "gulp-header": "^1.2.2", 37 | "gulp-load-plugins": "^0.10.0", 38 | "gulp-minify-css": "^1.2.1", 39 | "gulp-order": "^1.1.1", 40 | "gulp-plumber": "^1.0.0", 41 | "gulp-pug": "^3.0.4", 42 | "gulp-rename": "^1.2.2", 43 | "gulp-size": "^1.2.1", 44 | "gulp-sourcemaps": "^1.5.2", 45 | "gulp-stylus": "^2.0.1", 46 | "gulp-template-store": "^1.1.1", 47 | "gulp-uglify": "^1.4.1", 48 | "gulp-util": "^3.0.4", 49 | "gulp-wrap": "^0.11.0", 50 | "jquery": "^3.1.1", 51 | "jquery-ui": "^1.12.1", 52 | "jquery-ui-touch-punch": "^0.2.3", 53 | "lodash": "3.9.3", 54 | "marked": "^0.3.9", 55 | "material-design-icons": "^3.0.1", 56 | "material-design-lite": "^1.2.1", 57 | "mocha": "^3.2.0", 58 | "mocha-phantomjs": "^4.1.0", 59 | "normalize.css": "^5.0.0", 60 | "phantomjs": "^2.1.1", 61 | "vinyl-buffer": "^1.0.0", 62 | "vinyl-file": "^2.0.0", 63 | "vinyl-source-stream": "^1.1.0" 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/style/modules/task.styl: -------------------------------------------------------------------------------- 1 | /*** 2 | 3 | Tasks. 4 | 5 | ***/ 6 | 7 | taskMinHeight = 150px 8 | 9 | .tyto-task 10 | margin-bottom 30px 11 | padding 0 0 20px 0 12 | max-width 100% 13 | min-height taskMinHeight 14 | overflow visible 15 | z-index auto 16 | /* Adds extra drop shadow when task is being lifted/dragged */ 17 | &.ui-sortable-helper 18 | box-shadow 0 4px 4px 0 rgba(0,0,0,.14), 19 | 0 6px 2px -4px rgba(0,0,0,.2), 20 | 0 2px 10px 0 rgba(0,0,0,.12) 21 | /* Manages show/hide of opaque element interaction icons */ 22 | .does--fade 23 | opacity 0 24 | &:hover 25 | .does--fade 26 | &:hover 27 | opacity 1 28 | opacity 0.4 29 | 30 | &__header 31 | position relative 32 | margin 10px 0 33 | 34 | &__title 35 | display inline-block 36 | max-width 200px 37 | min-width 150px 38 | margin 0 39 | font-size 24px 40 | line-height 32px 41 | 42 | &__menu-btn 43 | position absolute 44 | top 5px 45 | right 5px 46 | width 24px 47 | height 24px 48 | border-radius 50% 49 | min-width 24px 50 | padding 0 51 | 52 | &__time 53 | padding 16px 54 | & > * 55 | opacity 0.4 56 | 57 | &__mover 58 | position absolute 59 | top 5px 60 | left 5px 61 | cursor pointer 62 | 63 | &__edit-wrapper 64 | position relative 65 | 66 | &__description 67 | width 100% 68 | a 69 | z-index 99999 70 | cursor pointer 71 | img 72 | display block 73 | margin 5px auto 74 | max-width 100% 75 | 76 | &__description-edit 77 | outline 0 78 | border none 79 | background transparent 80 | padding 16px 0 81 | width 258px 82 | max-height 100px 83 | margin 0 16px 84 | font-size 16px 85 | line-height 16px 86 | resize none 87 | 88 | &__placeholder 89 | height taskMinHeight - 50px 90 | width 90% 91 | margin 0em auto 1em auto 92 | -------------------------------------------------------------------------------- /src/markup/templates/edit.pug: -------------------------------------------------------------------------------- 1 | include ../mixins/menu.pug 2 | include ../mixins/timeLabel.pug 3 | 4 | .tyto-edit__nav 5 | |<% if (tyto.isNew) { %> 6 | button.tyto-edit__save.mdl-button.mdl-js-button.mdl-js-ripple-effect Done 7 | a.tyto-edit__cancel.mdl-button.mdl-js-button.mdl-js-ripple-effect(href!="#board/<%= tyto.board.id %>") Cancel 8 | |<% } else { %> 9 | a.tyto-edit__back.mdl-button.mdl-js-button.mdl-js-ripple-effect(href!="#board/<%= tyto.board.id %>") Return to board 10 | |<% } %> 11 | .tyto-edit__content 12 | .tyto-edit__details.has--bottom-margin 13 | h1.tyto-edit__task-title(contenteditable="true", data-model-prop="title", title="Task title") <%= tyto.title %> 14 | .tyto-edit__task-description(title="Task description") <%= tyto.description %> 15 | .tyto-task__edit-wrapper 16 | textarea.tyto-edit__edit-description.is--hidden(data-model-prop="description") 17 | .tyto-task__suggestions.tyto-suggestions__container.mdl-shadow--2dp.is--hidden 18 | .tyto-edit__details-footer.tx--right.has--bottom-margin 19 | +timeLabel('tyto-edit__task-time') 20 | .tyto-edit__task-column 21 | |<% if (tyto.selectedColumn) { %> 22 | |<%= tyto.selectedColumn.attributes.title %> 23 | |<% } %> 24 | .tyto-edit__actions 25 | |<% if (tyto.columns.length > 0 ) { %> 26 | +actionMenu('column-select', 'view_column', 'tyto-edit__column-select', '', 'bottom-right', 'Select column') 27 | |<% _.forEach(tyto.columns, function(column) { %> 28 | |<% if (!tyto.isNew) { var activeClass = (column.attributes.id === tyto.columnId) ? 'is--selected': '' } %> 29 | li.mdl-menu__item.tyto-edit__column-select__menu-option(data-column-id!="<%= column.id %>" class!="<%= activeClass %>") <%= column.attributes.title %> 30 | |<% }); %> 31 | |<% } %> 32 | +actionMenu('color-select' , 'color_lens', 'tyto-edit__color-select', '', 'bottom-right', 'Change color') 33 | |<% _.forEach(tyto.colors, function(col) { %> 34 | |<% var activeColor = (col === tyto.color) ? 'is--selected': '' %> 35 | li.mdl-menu__item.tyto-edit__color-select__menu-option(class!="bg--<%= col %> hv--<%= col %> <%= tyto.activeColor %>", data-color!="<%= col %>", title!="<%= col %>") 36 | |<% }); %> 37 | button.tyto-edit__track.mdl-button.mdl-js-button.mdl-button--icon.mdl-js-ripple-effect(title="Start tracking") 38 | i.material-icons schedule 39 | -------------------------------------------------------------------------------- /src/script/app.js: -------------------------------------------------------------------------------- 1 | // Create app instance 2 | import TytoApp from './config/tyto'; 3 | const Tyto = new TytoApp(); 4 | 5 | window.Tyto = Tyto; 6 | 7 | // Hydrate template store for views 8 | import Templates from './templates/templates'; 9 | Tyto.TemplateStore = Templates; 10 | 11 | // Import requirements 12 | import TytoCtrl from './controllers/tyto'; 13 | import TytoViews from './views/tyto'; 14 | import TytoModels from './models/tyto'; 15 | import TytoUtils from './utils/utils'; 16 | import TytoSuggestions from './utils/suggestions'; 17 | 18 | Tyto.module('Models', TytoModels); 19 | Tyto.module('Ctrl', TytoCtrl); 20 | Tyto.module('Views', TytoViews); 21 | Tyto.module('Utils', TytoUtils); 22 | Tyto.module('Suggestions', TytoSuggestions); 23 | 24 | Tyto.Boards = new Tyto.Models.BoardCollection(); 25 | Tyto.Columns = new Tyto.Models.ColumnCollection(); 26 | Tyto.Tasks = new Tyto.Models.TaskCollection(); 27 | Tyto.ActiveBoard = new Tyto.Models.Board(); 28 | Tyto.ActiveCols = new Tyto.Models.ColumnCollection(); 29 | Tyto.ActiveTasks = new Tyto.Models.TaskCollection(); 30 | 31 | Tyto.on('before:start', function() { 32 | return Tyto.setRootLayout(); 33 | }); 34 | 35 | Tyto.on('start', function() { 36 | Tyto.__renderer = new marked.Renderer(); 37 | Tyto.__renderer.link = function(href, title, text) { 38 | var e, out, prot; 39 | if (this.options.sanitize) { 40 | try { 41 | prot = decodeURIComponent(unescape(href)).replace(/[^\w:]/g, '').toLowerCase(); 42 | } catch (_error) { 43 | e = _error; 44 | return ''; 45 | } 46 | if (prot.indexOf('javascript:') === 0 || prot.indexOf('vbscript:') === 0) { 47 | return ''; 48 | } 49 | } 50 | out = ''; 55 | return out; 56 | }; 57 | marked.setOptions({ 58 | renderer: Tyto.__renderer 59 | }); 60 | Tyto.Controller = new Tyto.Ctrl.Controller(); 61 | Tyto.Controller.Router = new Tyto.Ctrl.Router({ 62 | controller: Tyto.Controller 63 | }); 64 | Tyto.Controller.start(); 65 | return Backbone.history.start(); 66 | }); 67 | 68 | 69 | /* 70 | In a scenario where we are interacting with a live backend, expect to use 71 | something similar to; 72 | 73 | Tyto.boardList.fetch().done (data) -> 74 | Tyto.start() 75 | 76 | However, as we are only loading from localStorage, we can reset collections 77 | based on what is stored in localStorage. 78 | 79 | For this we use a utility function implementing in the Utils module. 80 | */ 81 | 82 | Tyto.Utils.load(window.localStorage); 83 | Tyto.start(); 84 | -------------------------------------------------------------------------------- /src/script/controllers/tyto.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Global App Controller 3 | */ 4 | const AppCtrl = function(AppCtrl, App, Backbone, Marionette) { 5 | AppCtrl.Router = Marionette.AppRouter.extend({ 6 | appRoutes: { 7 | 'board/:board' : 'showBoardView', 8 | 'board/:board/task/:task' : 'showEditView', 9 | 'board/:board/task/:task?:params': 'showEditView', 10 | '*path' : 'showSelectView' 11 | } 12 | }); 13 | AppCtrl.Controller = Marionette.Controller.extend({ 14 | start: function() { 15 | this.showMenu(); 16 | if (window.localStorage && !window.localStorage.tyto) { 17 | this.showCookieBanner(); 18 | } 19 | }, 20 | showSelectView: function() { 21 | Tyto.SelectView = new App.Views.Select({ 22 | collection: Tyto.Boards 23 | }); 24 | Tyto.RootView.showChildView('Content', Tyto.SelectView); 25 | }, 26 | showMenu: function() { 27 | Tyto.MenuView = new App.Views.Menu(); 28 | Tyto.RootView.showChildView('Menu', Tyto.MenuView); 29 | }, 30 | showCookieBanner: function() { 31 | /* 32 | Show cookie banner by creating a temporary region and showing 33 | the view. 34 | */ 35 | Tyto.RootView.$el.prepend($('')); 36 | Tyto.RootView.addRegion('Cookie', '#cookie-banner'); 37 | Tyto.CookieBannerView = new App.Views.CookieBanner(); 38 | Tyto.RootView.showChildView('Cookie', Tyto.CookieBannerView); 39 | }, 40 | showBoardView: function(id) { 41 | let cols, model, tasks; 42 | Tyto.ActiveBoard = model = Tyto.Boards.get(id); 43 | if (model) { 44 | cols = Tyto.Columns.where({ 45 | boardId: model.id 46 | }); 47 | tasks = Tyto.Tasks.where({ 48 | boardId: model.id 49 | }); 50 | Tyto.ActiveTasks.reset(tasks); 51 | Tyto.ActiveCols.reset(cols); 52 | Tyto.BoardView = new App.Views.Board({ 53 | model: model, 54 | collection: Tyto.ActiveCols, 55 | options: { 56 | tasks: Tyto.ActiveTasks 57 | } 58 | }); 59 | App.RootView.showChildView('Content', Tyto.BoardView); 60 | } else { 61 | App.navigate('/', true); 62 | } 63 | }, 64 | showEditView: function(bId, tId, params) { 65 | let taskToEdit; 66 | const board = Tyto.Boards.get(bId); 67 | const columns = Tyto.Columns.where({ 68 | boardId: bId 69 | }); 70 | let parentColumn; 71 | let isNew = false; 72 | if (params) { 73 | let qS = Tyto.Utils.processQueryString(params); 74 | if (qS.isFresh === 'true') { 75 | isNew = true; 76 | taskToEdit = Tyto.TempTask = new Tyto.Models.Task({ 77 | boardId: bId, 78 | id : tId 79 | }); 80 | } 81 | } else { 82 | taskToEdit = Tyto.Tasks.get(tId); 83 | } 84 | if (taskToEdit && board) { 85 | Tyto.EditView = new App.Views.Edit({ 86 | model : taskToEdit, 87 | board : board, 88 | columns: columns, 89 | isNew : isNew 90 | }); 91 | App.RootView.showChildView('Content', Tyto.EditView); 92 | } else if (board) { 93 | Tyto.navigate('/board/' + board.id, true); 94 | } else { 95 | Tyto.navigate('/', true); 96 | } 97 | } 98 | }); 99 | }; 100 | 101 | export default AppCtrl; 102 | -------------------------------------------------------------------------------- /src/script/views/time.js: -------------------------------------------------------------------------------- 1 | const TimeModal = Backbone.Marionette.ItemView.extend({ 2 | template: function(args) { 3 | return Tyto.TemplateStore.timeModal(args); 4 | }, 5 | className: function() { 6 | return this.domAttributes.VIEW_CLASS; 7 | }, 8 | domAttributes: { 9 | VIEW_CLASS: 'tyto-time-modal', 10 | PLAY_ICON : 'play_arrow', 11 | PAUSE_ICON: 'pause' 12 | }, 13 | ui: { 14 | timerBtn : '.tyto-time-modal__timer', 15 | taskDescription: '.tyto-time-modal__content-description', 16 | timerIcon : '.tyto-time-modal__timer-icon', 17 | resetBtn : '.tyto-time-modal__timer-reset', 18 | closeBtn : '.tyto-time-modal__close', 19 | timeLbl : '.tyto-time-modal__timer-lbl', 20 | hours : '.tyto-time-modal__timer-lbl-hours', 21 | minutes : '.tyto-time-modal__timer-lbl-minutes', 22 | seconds : '.tyto-time-modal__timer-lbl-seconds' 23 | }, 24 | events: { 25 | 'click @ui.closeBtn': 'closeModal', 26 | 'click @ui.timerBtn': 'toggleTimer', 27 | 'click @ui.resetBtn': 'resetTimer' 28 | }, 29 | startTimer: function() { 30 | const view = this; 31 | view.isTiming = true; 32 | view.ui.timerIcon.text(view.domAttributes.PAUSE_ICON); 33 | view.ui.resetBtn.attr('disabled', true); 34 | view.ui.resetBtn.removeClass('mdl-button--accent'); 35 | view.ui.closeBtn.attr('disabled', true); 36 | view.ui.closeBtn.removeClass('mdl-button--accent'); 37 | view.timingInterval = setInterval(function() { 38 | view.incrementTime(); 39 | view.renderTime(); 40 | }, 1000); 41 | }, 42 | incrementTime: function() { 43 | const view = this; 44 | const time = view.model.get('timeSpent'); 45 | time.seconds++; 46 | if (time.seconds >= 60) { 47 | time.seconds = 0; 48 | time.minutes++; 49 | if (time.minutes >= 60) { 50 | time.minutes = 0; 51 | return time.hours++; 52 | } 53 | } 54 | }, 55 | renderTime: function() { 56 | const view = this; 57 | const newTime = Tyto.Utils.getRenderFriendlyTime(view.model.get('timeSpent')); 58 | for(let measure of ['hours', 'minutes', 'seconds']) { 59 | if (view.ui[measure].text() !== newTime[measure]) 60 | view.ui[measure].text(newTime[measure]); 61 | } 62 | }, 63 | onRender: function() { 64 | const view = this; 65 | view.ui.taskDescription.html(marked(view.model.get('description'))); 66 | view.renderTime(); 67 | }, 68 | stopTimer: function() { 69 | const view = this; 70 | view.isTiming = false; 71 | view.ui.timerIcon.text(view.domAttributes.PLAY_ICON); 72 | view.ui.resetBtn.removeAttr('disabled'); 73 | view.ui.resetBtn.addClass('mdl-button--accent'); 74 | view.ui.closeBtn.removeAttr('disabled'); 75 | view.ui.closeBtn.addClass('mdl-button--accent'); 76 | clearInterval(view.timingInterval); 77 | }, 78 | resetTimer: function() { 79 | const view = this; 80 | view.model.set('timeSpent', { 81 | hours: 0, 82 | minutes: 0, 83 | seconds: 0 84 | }); 85 | view.renderTime(); 86 | }, 87 | toggleTimer: function() { 88 | const view = this; 89 | if (view.isTiming) { 90 | view.stopTimer(); 91 | } else { 92 | view.startTimer(); 93 | } 94 | }, 95 | closeModal: function() { 96 | const view = this; 97 | view.model.save({ 98 | timeSpent: view.model.get('timeSpent') 99 | }); 100 | Tyto.RootView.getRegion('TimeModal').$el.remove(); 101 | Tyto.RootView.removeRegion('TimeModal'); 102 | Tyto.Utils.renderTime(view.options.modelView); 103 | view.destroy(); 104 | } 105 | }); 106 | 107 | export default TimeModal; 108 | -------------------------------------------------------------------------------- /src/script/views/menu.js: -------------------------------------------------------------------------------- 1 | const MenuView = Backbone.Marionette.ItemView.extend({ 2 | template: function(args) { 3 | return Tyto.TemplateStore.menu(args); 4 | }, 5 | tagName: 'div', 6 | className: function() { 7 | return this.domAttributes.VIEW_CLASS; 8 | }, 9 | ui: { 10 | addBoardBtn: '.tyto-menu__add-board', 11 | exportBtn : '.tyto-menu__export', 12 | loadBtn : '.tyto-menu__load', 13 | importBtn : '.tyto-menu__import', 14 | deleteBtn : '.tyto-menu__delete-save', 15 | exporter : '.tyto-menu__exporter', 16 | importer : '.tyto-menu__importer', 17 | action : 'button' 18 | }, 19 | events: { 20 | 'click @ui.addBoardBtn': 'addBoard', 21 | 'click @ui.exportBtn' : 'exportData', 22 | 'click @ui.deleteBtn' : 'deleteAppData', 23 | 'click @ui.loadBtn' : 'initLoad', 24 | 'click @ui.importBtn' : 'initLoad', 25 | 'click @ui.action' : 'restoreContent', 26 | 'change @ui.importer' : 'handleFile' 27 | }, 28 | props: { 29 | DOWNLOAD_FILE_NAME: 'barn.json' 30 | }, 31 | domAttributes: { 32 | VIEW_CLASS : 'tyto-menu', 33 | MENU_VISIBLE_CLASS: 'is-visible' 34 | }, 35 | onShow: function() { 36 | const view = this; 37 | /** 38 | * The MenuView of Tyto handles the JSON import and export for the 39 | * application making use of the 'Utils' modules' 'load' function. 40 | */ 41 | view.reader = new FileReader(); 42 | view.reader.onloadend = function(e) { 43 | const data = JSON.parse(e.target.result); 44 | if (view.activeImporter.id === view.ui.loadBtn.attr('id')) { 45 | Tyto.Utils.load(data, false, true); 46 | } else { 47 | Tyto.Utils.load(data, true, false); 48 | } 49 | Tyto.navigate('/', true); 50 | }; 51 | }, 52 | restoreContent: function() { 53 | const props = this.domAttributes; 54 | const $visibles = Tyto.RootView.getRegion('Menu').$el.parent().find(`.${props.MENU_VISIBLE_CLASS}`); 55 | $visibles.removeClass(props.MENU_VISIBLE_CLASS); 56 | }, 57 | handleFile: function(e) { 58 | const view = this; 59 | const file = e.target.files[0]; 60 | if ((file.type.match('application/json')) || (file.name.indexOf('.json' !== -1))) { 61 | view.reader.readAsText(file); 62 | this.ui.importer[0].value = null; 63 | } else { 64 | alert('[tyto] only valid json files allowed'); 65 | } 66 | }, 67 | initLoad: function(e) { 68 | this.activeImporter = e.currentTarget; 69 | const anchor = this.ui.importer[0]; 70 | if (window.File && window.FileReader && window.FileList && window.Blob) { 71 | anchor.click(); 72 | } else { 73 | alert('[tyto] Unfortunately the file APIs are not fully supported in your browser'); 74 | } 75 | }, 76 | exportData: function() { 77 | const view = this; 78 | const anchor = view.ui.exporter[0]; 79 | const exportable = {}; 80 | _.forOwn(window.localStorage, function(val, key) { 81 | if (key.indexOf('tyto') !== -1) { 82 | return exportable[key] = val; 83 | } 84 | }); 85 | const filename = view.props.DOWNLOAD_FILE_NAME; 86 | const content = `data:text/plain,${JSON.stringify(exportable)}`; 87 | anchor.setAttribute('download', filename); 88 | anchor.setAttribute('href', content); 89 | anchor.click(); 90 | }, 91 | deleteAppData: function() { 92 | _.forOwn(window.localStorage, function(val, key) { 93 | if (key.indexOf('tyto') !== -1 && key !== 'tyto') { 94 | return window.localStorage.removeItem(key); 95 | } 96 | }); 97 | Tyto.Boards.reset(); 98 | Tyto.Columns.reset(); 99 | Tyto.Tasks.reset(); 100 | Tyto.navigate('/', true); 101 | }, 102 | addBoard: function() { 103 | Tyto.navigate('board/' + Tyto.Boards.create().id, true); 104 | } 105 | }); 106 | 107 | export default MenuView; 108 | -------------------------------------------------------------------------------- /gulp-config.js: -------------------------------------------------------------------------------- 1 | var env = 'public/', 2 | vendorDir = 'node_modules/', 3 | pkg = require('./package.json'); 4 | module.exports = { 5 | pkg: { 6 | name: pkg.name 7 | }, 8 | pluginOpts: { 9 | pug: { 10 | data : { 11 | name : pkg.name, 12 | description: pkg.description 13 | } 14 | }, 15 | coffee: { 16 | bare: true 17 | }, 18 | minify: { 19 | keepSpecialComments: 1 20 | }, 21 | gSize: { 22 | showFiles: true 23 | }, 24 | browserSync: { 25 | port : 1987, 26 | startPath: '/public', 27 | server : { 28 | baseDir: './' 29 | } 30 | }, 31 | uglify: { 32 | preserveComments: 'license' 33 | }, 34 | rename: { 35 | suffix: '.min' 36 | }, 37 | order: { 38 | stylus: [ 39 | '_var.stylus', 40 | '_typography.stylus', 41 | '_functions.stylus', 42 | 'base.stylus', 43 | '**/*.stylus' 44 | ] 45 | }, 46 | prefix: [ 47 | 'last 3 versions', 48 | 'Blackberry 10', 49 | 'Android 3', 50 | 'Android 4' 51 | ], 52 | wrap: '(function() { <%= contents %> }());', 53 | load: { 54 | rename: { 55 | 'gulp-gh-pages' : 'deploy', 56 | 'gulp-util' : 'gUtil', 57 | 'gulp-minify-css' : 'minify', 58 | 'gulp-autoprefixer' : 'prefix', 59 | 'gulp-template-store' : 'template' 60 | } 61 | } 62 | }, 63 | paths: { 64 | base : env, 65 | sources: { 66 | script: [ 67 | 'src/script/**/*.js' 68 | ], 69 | img: [ 70 | 'src/img/**/*.*' 71 | ], 72 | json: [ 73 | 'src/json/**/*.json' 74 | ], 75 | vendor: { 76 | js: [ 77 | vendorDir + 'jquery/dist/jquery.js', 78 | vendorDir + 'lodash/index.js', 79 | vendorDir + 'backbone/backbone.js', 80 | vendorDir + 'backbone.wreqr/lib/backbone.wreqr.js', 81 | vendorDir + 'backbone.localstorage/backbone.localStorage.js', 82 | vendorDir + 'backbone.marionette/lib/backbone.marionette.js', 83 | /** 84 | * In order to use jquery/jquery-ui, need to require specific 85 | * modules in order to get sortable working. 86 | */ 87 | vendorDir + 'jquery-ui/ui/data.js', 88 | vendorDir + 'jquery-ui/ui/widget.js', 89 | vendorDir + 'jquery-ui/ui/widgets/mouse.js', 90 | vendorDir + 'jquery-ui/ui/widgets/sortable.js', 91 | vendorDir + 'jquery-ui/ui/scroll-parent.js', 92 | vendorDir + 'jquery-ui/ui/version.js', 93 | vendorDir + 'jquery-ui/ui/ie.js', 94 | vendorDir + 'jquery-ui-touch-punch/jquery.ui.touch-punch.min.js', 95 | vendorDir + 'material-design-lite/material.js', 96 | vendorDir + 'marked/marked.min.js' 97 | ], 98 | css: [ 99 | vendorDir + 'normalize.css/normalize.css', 100 | vendorDir + 'material-design-lite/material.css' 101 | ], 102 | fonts: [ 103 | vendorDir + 'material-design-icons/iconfont/**/*.{eot,ttf,woff,woff2}' 104 | ] 105 | }, 106 | markup: [ 107 | 'src/markup/*.pug', 108 | 'src/markup/layout-blocks/**/*.pug' 109 | ], 110 | docs : 'src/markup/*.pug', 111 | templates: 'src/markup/templates/**/*.pug', 112 | style : 'src/style/**/*.styl', 113 | overwatch: env + '**/*.*' 114 | }, 115 | destinations: { 116 | html : env, 117 | js : env + 'js/', 118 | css : env + 'css/', 119 | img : env + 'img/', 120 | fonts : env + 'fonts/', 121 | templates: 'src/script/templates/', 122 | build : '', 123 | dist : './dist', 124 | test : 'testEnv/' 125 | } 126 | } 127 | }; 128 | -------------------------------------------------------------------------------- /src/script/views/task.js: -------------------------------------------------------------------------------- 1 | const TaskView = Backbone.Marionette.ItemView.extend({ 2 | tagName: 'div', 3 | className: function() { 4 | return this.domAttributes.VIEW_CLASS + this.model.attributes.color; 5 | }, 6 | attributes: function() { 7 | const attr = {}; 8 | attr[this.domAttributes.VIEW_ATTR] = this.model.get('id'); 9 | return attr; 10 | }, 11 | template: function(args) { 12 | return Tyto.TemplateStore.task(args); 13 | }, 14 | ui: { 15 | deleteTask : '.tyto-task__delete-task', 16 | editTask : '.tyto-task__edit-task', 17 | trackTask : '.tyto-task__track-task', 18 | description : '.tyto-task__description', 19 | title : '.tyto-task__title', 20 | menu : '.tyto-task__menu', 21 | hours : '.tyto-task__time__hours', 22 | minutes : '.tyto-task__time__minutes', 23 | time : '.tyto-task__time', 24 | editDescription: '.tyto-task__description-edit', 25 | suggestions : '.tyto-task__suggestions' 26 | }, 27 | events: { 28 | 'click @ui.deleteTask' : 'deleteTask', 29 | 'click @ui.editTask' : 'editTask', 30 | 'click @ui.trackTask' : 'trackTask', 31 | 'blur @ui.title' : 'saveTaskTitle', 32 | 'keydown @ui.title' : 'saveTaskTitle', 33 | 'blur @ui.editDescription': 'saveTaskDescription', 34 | 'click @ui.description' : 'showEditMode', 35 | 36 | /** 37 | * NOTE:: These are functions that are bootstrapped in from 38 | * the 'Suggestions' module. 39 | */ 40 | 'keypress @ui.editDescription': 'handleKeyInteraction', 41 | 'keydown @ui.editDescription' : 'handleKeyInteraction', 42 | 'keyup @ui.editDescription' : 'handleKeyInteraction', 43 | 'click @ui.suggestions' : 'selectSuggestion' 44 | }, 45 | domAttributes: { 46 | VIEW_CLASS : 'tyto-task mdl-card mdl-shadow--2dp bg--', 47 | VIEW_ATTR : 'data-task-id', 48 | IS_BEING_ADDED_CLASS: 'is--adding-task', 49 | COLUMN_CLASS : '.tyto-column', 50 | TASK_CONTAINER_CLASS: '.tyto-column__tasks', 51 | HIDDEN_UTIL_CLASS : 'is--hidden', 52 | INDICATOR : '.indicator' 53 | }, 54 | getMDLMap: function() { 55 | const view = this; 56 | return [ 57 | { 58 | el: view.ui.menu[0], 59 | component: 'MaterialMenu' 60 | } 61 | ]; 62 | }, 63 | handleKeyInteraction: function(e) { 64 | if (e.which === 27) this.saveTaskDescription(); 65 | this.filterItems(e); 66 | }, 67 | initialize: function() { 68 | const view = this; 69 | const attr = view.domAttributes; 70 | Tyto.Suggestions.bootstrapView(view); 71 | view.$el.on(Tyto.ANIMATION_EVENT, function() { 72 | $(this).parents(attr.COLUMN_CLASS).removeClass(attr.IS_BEING_ADDED_CLASS); 73 | }); 74 | }, 75 | deleteTask: function() { 76 | if (confirm(Tyto.CONFIRM_MESSAGE)) { 77 | this.model.destroy(); 78 | } 79 | }, 80 | onShow: function() { 81 | const view = this; 82 | const attr = view.domAttributes; 83 | const container = view.$el.parents(attr.TASK_CONTAINER_CLASS)[0]; 84 | const column = view.$el.parents(attr.COLUMN_CLASS); 85 | if (container.scrollHeight > container.offsetHeight) { 86 | container.scrollTop = container.scrollHeight; 87 | } 88 | Tyto.Utils.upgradeMDL(view.getMDLMap()); 89 | }, 90 | onRender: function() { 91 | const view = this; 92 | view.ui.description.html(marked(view.model.get('description'))); 93 | Tyto.Utils.autoSize(view.ui.editDescription[0]); 94 | Tyto.Utils.renderTime(view); 95 | }, 96 | trackTask: function(e) { 97 | Tyto.Utils.showTimeModal(this.model, this); 98 | }, 99 | editTask: function(e) { 100 | const view = this; 101 | const boardId = view.model.get('boardId'); 102 | const taskId = view.model.id; 103 | const editUrl = `#board/${boardId}/task/${taskId}`; 104 | Tyto.Utils.bloom(view.ui.editTask[0], view.model.get('color'), editUrl); 105 | }, 106 | showEditMode: function() { 107 | const domAttributes = this.domAttributes; 108 | const model = this.model; 109 | const desc = this.ui.description; 110 | const edit = this.ui.editDescription; 111 | desc.addClass(domAttributes.HIDDEN_UTIL_CLASS); 112 | edit.removeClass(domAttributes.HIDDEN_UTIL_CLASS) 113 | .val(model.get('description')) 114 | .focus(); 115 | }, 116 | saveTaskDescription: function(e) { 117 | const domAttributes = this.domAttributes; 118 | const edit = this.ui.editDescription; 119 | const desc = this.ui.description; 120 | edit.addClass(domAttributes.HIDDEN_UTIL_CLASS); 121 | desc.removeClass(domAttributes.HIDDEN_UTIL_CLASS); 122 | const content = edit.val(); 123 | this.model.save({ 124 | description: content 125 | }); 126 | desc.html(marked(content)); 127 | this.hideSuggestions(); 128 | }, 129 | saveTaskTitle: function(e) { 130 | this.model.save({ 131 | title: this.ui.title.text().trim() 132 | }); 133 | if (e.type === 'keydown' && e.which === 27) 134 | this.ui.title.blur(); 135 | } 136 | }); 137 | 138 | export default TaskView; 139 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://travis-ci.org/jh3y/tyto.svg?branch=master)](https://travis-ci.org/jh3y/tyto) 2 | tyto ![alt tag](https://raw.github.com/jh3y/tyto/master/src/img/tyto.png) 3 | === 4 | __tyto__ is an extensible and customizable management and organisation tool 5 | 6 | just visit [jh3y.github.io/tyto](http://jh3y.github.io/tyto)! 7 | 8 | ![alt tag](https://raw.github.com/jh3y/pics/master/tyto/app_three_cols.png) 9 | 10 | ### Features 11 | * minimal UI 12 | * no accounts necessary 13 | * intuitive 14 | * extensible 15 | * localStorage persistence 16 | * time tracking 17 | * sortable UI 18 | * task linking 19 | * Markdown support 20 | * etc. 21 | 22 | ![alt tag](https://raw.github.com/jh3y/pics/master/tyto/add_task.gif) 23 | 24 | ### Why tyto? What's it for? 25 | 26 | Tyto arose from the want for an electronic post-it board without the need for accounts. Something simple and intuitive that could be easily shared. 27 | 28 | It's also the product of my own curiosity being used as an opportunity to pick up new tech stacks. It started as a vanilla JS app utilising one file and experimenting with HTML5 drag and drop. It then grew a little more, and a little more after that. Now it uses Backbone w/ Marionette. The next step? Most likely Angular 2.0 or React. 29 | 30 | ![alt tag](https://raw.github.com/jh3y/pics/master/tyto/edit_view.png) 31 | 32 | In truth, most organisations have some form of tool for what Tyto is doing. In my experience though, they can be cumbersome, clunky and just a bit noisy. Some employees tend to dislike internal tools. You still see whiteboards and walls plastered in sticky notes. 33 | 34 | This is where Tyto came from, It's my personal intuitive and minimal TodoMVC. No accounts necessary and the source isn't too hard to grasp making it rather easy to extend and customise. 35 | 36 | ![alt tag](https://raw.github.com/jh3y/pics/master/tyto/edit_task.gif) 37 | 38 | ### Who's it for? 39 | Developer and project managers were the original target audience. A means to share project progression on a more _personal_ level. As opposed to publicly through an internal system. Almost like a complimentary attachment to an email. 40 | 41 | ![alt tag](https://raw.github.com/jh3y/pics/master/tyto/change_color.gif) 42 | 43 | There are no restrictions though, it's open source. Not quite right out of the box? Change it :smile: 44 | 45 | Extensibility provides a means to create a bespoke version based on theme or functionality. 46 | 47 | Tyto is a personal pet of mine and if it can help others, that's great! 48 | 49 | 50 | 51 | ###Using tyto 52 | Just want to use it? Do that by visiting [jh3y.github.io/tyto](http://jh3y.github.io/tyto). 53 | 54 | GitHub flavored markdown is supported thanks to `marked`. 55 | 56 | ![alt tag](https://raw.github.com/jh3y/pics/master/tyto/markdown.gif) 57 | 58 | This also enables you to link to boards, columns and other tasks by using the `#` character 59 | 60 | ![alt tag](https://raw.github.com/jh3y/pics/master/tyto/linking.gif) 61 | 62 | Changes are persistent thanks to `localStorage`. 63 | 64 | Want to move to a different browser or machine though? Use the export utility to export a json file. Load this using the import utility. 65 | 66 | A persistent workflow across devices? I'm afraid I haven't implemented that. Accounts is _not_ something I am keen on implementing/hosting right now. I think it diverts from my original intention with Tyto. 67 | 68 | #### Your own environment 69 | ##### Prerequisites 70 | If you're cloning the repo and setting up the codebase you are going to need __node__(_preferably __yarn___) and __gulp__ installed. 71 | 72 | ##### Set up 73 | 1. Clone the repo. 74 | 75 | git clone https://github.com/jh3y/tyto.git 76 | 77 | 2. Navigate into the repo and install the dependencies. 78 | 79 | cd tyto 80 | yarn (alternatively, npm install) 81 | 82 | 3. Run gulp to take care of preprocessing and running a local static server instance(project utilises BrowserSync). 83 | 84 | gulp 85 | 86 | #### Hosting 87 | I would suggest just taking a snapshot of the `gh-pages` branch and ftp'ing this onto your desired server or web space. Alternatively, follow the set up procedure and FTP the contents of the `public` directory. 88 | 89 | If you wish to host on Github. Follow the set up procedure first(ideally, with a fork). When happy with your version, use the `deploy` task. This will require familiarity with `gulp-gh-pages` in order to publish to the correct location if other than `gh-pages`. 90 | 91 | ![alt tag](https://raw.github.com/jh3y/pics/master/tyto/speed_dial.gif) 92 | 93 | ### Development 94 | A strength of tyto is extensibility. Making changes whether it be functional or aesthetic is straightforward once familiar with the codebase. 95 | 96 | Any queries as to how things work in the codebase? Feel free to raise an issue with a question! 97 | 98 | ![alt tag](https://raw.github.com/jh3y/pics/master/tyto/time_track.gif) 99 | 100 | ####Under the hood 101 | There are a range of technologies being used under the hood. 102 | * jQuery 103 | * jQuery UI 104 | * Material Design Lite 105 | * Lodash 106 | * Backbone 107 | * Marionette 108 | * Marked 109 | * Jade 110 | * Stylus 111 | * Babel 112 | * Gulp 113 | 114 | ![alt tag](https://raw.github.com/jh3y/pics/master/tyto/move_task.gif) 115 | 116 | ### License 117 | 118 | MIT 119 | 120 | --------------------------- 121 | 122 | Made with :sparkles: [@jh3y](https://twitter.com/@_jh3y) 2017 123 | -------------------------------------------------------------------------------- /src/json/intro.json: -------------------------------------------------------------------------------- 1 | {"tyto":"true","tyto--board":"b2206024-5879-3471-86e5-a2d3cdc6bbdf","tyto--board-b2206024-5879-3471-86e5-a2d3cdc6bbdf":"{\"title\":\"Intro Board\",\"id\":\"b2206024-5879-3471-86e5-a2d3cdc6bbdf\"}","tyto--column":"8023a94e-36c4-4ecc-b2b9-772b27a547af,a6fb9c29-7d8e-f1fa-77f6-12461f151e0d,d993b375-5621-398d-6048-1ec53123b781,ec6edef3-7f75-ff9c-a018-a0a27da7d3f3,f881472f-b490-0975-3ff3-ef04dd4ff3b3","tyto--column-8023a94e-36c4-4ecc-b2b9-772b27a547af":"{\"boardId\":\"b2206024-5879-3471-86e5-a2d3cdc6bbdf\",\"ordinal\":3,\"title\":\"Boards\",\"id\":\"8023a94e-36c4-4ecc-b2b9-772b27a547af\"}","tyto--column-a6fb9c29-7d8e-f1fa-77f6-12461f151e0d":"{\"boardId\":\"b2206024-5879-3471-86e5-a2d3cdc6bbdf\",\"ordinal\":4,\"title\":\"Columns\",\"id\":\"a6fb9c29-7d8e-f1fa-77f6-12461f151e0d\"}","tyto--column-d993b375-5621-398d-6048-1ec53123b781":"{\"boardId\":\"b2206024-5879-3471-86e5-a2d3cdc6bbdf\",\"ordinal\":2,\"title\":\"Menu\",\"id\":\"d993b375-5621-398d-6048-1ec53123b781\"}","tyto--column-ec6edef3-7f75-ff9c-a018-a0a27da7d3f3":"{\"boardId\":\"b2206024-5879-3471-86e5-a2d3cdc6bbdf\",\"ordinal\":5,\"title\":\"Tasks\",\"id\":\"ec6edef3-7f75-ff9c-a018-a0a27da7d3f3\"}","tyto--column-f881472f-b490-0975-3ff3-ef04dd4ff3b3":"{\"boardId\":\"b2206024-5879-3471-86e5-a2d3cdc6bbdf\",\"ordinal\":1,\"title\":\"Interacting with the UI\",\"id\":\"f881472f-b490-0975-3ff3-ef04dd4ff3b3\"}","tyto--task":"25370d57-8f9f-33fd-ac39-a4cb809bd1f0,2cf2ae78-d3ed-0dd5-69d4-74e63bf1c7b7,9a31bc5c-41f3-fc3c-dbec-4ccc042914bf,b21ab6e4-1ef7-0a73-f7ef-e5918e834d72,e5ccc8b3-6e22-d5d3-b0f6-8887c4b10f9d","tyto--task-25370d57-8f9f-33fd-ac39-a4cb809bd1f0":"{\"columnId\":\"ec6edef3-7f75-ff9c-a018-a0a27da7d3f3\",\"boardId\":\"b2206024-5879-3471-86e5-a2d3cdc6bbdf\",\"ordinal\":1,\"title\":\"Handling Tasks\",\"description\":\"Tasks are created via three separate ways.

You have the option to add tasks via a separate page or use a 'quick add' style.

To quickly add tasks, use the plus icon at the bottom of a column or use the option from a columns menu.

Otherwise, use the option from the primary action button in the bottom right.

To delete a task, choose the delete option from it's menu (located top right).

To move a task within it's column or possibly to another column, drag it, using the move icon in the top left.

You can also edit a task on a separate page giving you some further options by using the edit option from the task menu.
\",\"id\":\"25370d57-8f9f-33fd-ac39-a4cb809bd1f0\",\"color\":\"purple\"}","tyto--task-2cf2ae78-d3ed-0dd5-69d4-74e63bf1c7b7":"{\"columnId\":\"d993b375-5621-398d-6048-1ec53123b781\",\"boardId\":\"b2206024-5879-3471-86e5-a2d3cdc6bbdf\",\"ordinal\":1,\"title\":\"Menu options\",\"description\":\"Via the top level menu (accessed via top left) you can create new boards.

You are also able to import, export and load your data from tyto using options in the menu.

The difference between loading and importing is that a load will load over whatever is currently in place whereas an import will add to your current boards.

You can also wipe the localStorage for tyto, effectively wiping all of your data using the 'Delete Data' option.
\",\"id\":\"2cf2ae78-d3ed-0dd5-69d4-74e63bf1c7b7\",\"color\":\"orange\"}","tyto--task-9a31bc5c-41f3-fc3c-dbec-4ccc042914bf":"{\"columnId\":\"a6fb9c29-7d8e-f1fa-77f6-12461f151e0d\",\"boardId\":\"b2206024-5879-3471-86e5-a2d3cdc6bbdf\",\"ordinal\":1,\"title\":\"Interacting with Columns\",\"description\":\"You can have multiple columns per board. The current maximum is 10.

Add a new column via the primary action button in the bottom right.

You can delete a column via each columns' menu in the top right and you can move a column by dragging the move icon to change the order and placement of columns.

Column titles are editable and you need just click the title, make changes, and they will be updated.
\",\"id\":\"9a31bc5c-41f3-fc3c-dbec-4ccc042914bf\",\"color\":\"red\"}","tyto--task-b21ab6e4-1ef7-0a73-f7ef-e5918e834d72":"{\"columnId\":\"8023a94e-36c4-4ecc-b2b9-772b27a547af\",\"boardId\":\"b2206024-5879-3471-86e5-a2d3cdc6bbdf\",\"ordinal\":1,\"title\":\"What's a Board?\",\"description\":\"A board consists of a number of columns and tasks.

You can create multiple boards for different purposes.

You can then switch between boards using the dropdown next to the current board name.

To edit a board name, simply click it's name and make changes.

To add a new board, open the side menu(top left) and select 'Add a new board'.

To delete a board, use the menu on the top right of the page. Here you can also wipe the board clean or email the contents of a board.
\",\"id\":\"b21ab6e4-1ef7-0a73-f7ef-e5918e834d72\",\"color\":\"yellow\"}","tyto--task-e5ccc8b3-6e22-d5d3-b0f6-8887c4b10f9d":"{\"columnId\":\"f881472f-b490-0975-3ff3-ef04dd4ff3b3\",\"boardId\":\"b2206024-5879-3471-86e5-a2d3cdc6bbdf\",\"ordinal\":1,\"title\":\"Interaction\",\"description\":\"The aim for tyto's UI is to be minimal and clean.

Interaction points will disappear unless the user is hovering over relevant parts of the UI. This reduces clutter for the eye.

Editable items such as column name, board name, task title & description can be edited by simply clicking on the desired text.

Adding columns and tasks to a board is handled by the primary action button located in the bottom right of the screen with the plus icon.
\",\"id\":\"e5ccc8b3-6e22-d5d3-b0f6-8887c4b10f9d\",\"color\":\"blue\"}"} 2 | -------------------------------------------------------------------------------- /src/script/views/column.js: -------------------------------------------------------------------------------- 1 | import Task from './task'; 2 | 3 | const ColumnView = Backbone.Marionette.CompositeView.extend({ 4 | tagName: 'div', 5 | className: function() { 6 | return this.domAttributes.VIEW_CLASS; 7 | }, 8 | attributes: function() { 9 | const attr = {}; 10 | attr[this.domAttributes.VIEW_ATTR] = this.model.get('id'); 11 | return attr; 12 | }, 13 | template: function(args) { 14 | return Tyto.TemplateStore.column(args); 15 | }, 16 | childView: Task, 17 | childViewContainer: function() { 18 | return this.domAttributes.CHILD_VIEW_CONTAINER_CLASS; 19 | }, 20 | events: { 21 | 'click @ui.deleteColumn': 'deleteColumn', 22 | 'click @ui.addTask' : 'addTask', 23 | 'blur @ui.columnTitle' : 'updateTitle' 24 | }, 25 | ui: { 26 | deleteColumn : '.tyto-column__delete-column', 27 | addTask : '.tyto-column__add-task', 28 | columnTitle : '.tyto-column__title', 29 | taskContainer: '.tyto-column__tasks', 30 | columnMenu : '.tyto-column__menu' 31 | }, 32 | collectionEvents: { 33 | 'destroy': 'handleTaskRemoval' 34 | }, 35 | domAttributes: { 36 | VIEW_CLASS : 'tyto-column', 37 | VIEW_ATTR : 'data-col-id', 38 | PARENT_CONTAINER_CLASS : '.tyto-board__columns', 39 | CHILD_VIEW_CONTAINER_CLASS: '.tyto-column__tasks', 40 | BOARD_CLASS : '.tyto-board', 41 | COLUMN_ADD_CLASS : 'is--adding-column', 42 | TASK_ADD_CLASS : 'is--adding-task', 43 | TASK_ATTR : 'data-task-id', 44 | TASK_CLASS : '.tyto-task', 45 | TASK_MOVER_CLASS : '.tyto-task__mover', 46 | TASK_PLACEHOLDER_CLASS : 'tyto-task__placeholder' 47 | }, 48 | getMDLMap: function() { 49 | const view = this; 50 | return [ 51 | { 52 | el: view.ui.columnMenu[0], 53 | component: 'MaterialMenu' 54 | } 55 | ]; 56 | }, 57 | handleTaskRemoval: function(e) { 58 | const view = this; 59 | const attr = view.domAttributes; 60 | const list = Array.prototype.slice.call(view.$el.find(attr.TASK_CLASS)); 61 | Tyto.Utils.reorder(view, list, attr.TASK_ATTR); 62 | }, 63 | initialize: function() { 64 | const view = this; 65 | const attr = view.domAttributes; 66 | view.$el.on(Tyto.ANIMATION_EVENT, function() { 67 | view.$el.parents(attr.BOARD_CLASS).removeClass(attr.COLUMN_ADD_CLASS); 68 | }); 69 | }, 70 | onBeforeRender: function() { 71 | this.collection.models = this.collection.sortBy('ordinal'); 72 | }, 73 | bindTasks: function() { 74 | const view = this; 75 | const attr = view.domAttributes; 76 | view.ui.taskContainer.sortable({ 77 | connectWith: attr.CHILD_VIEW_CONTAINER_CLASS, 78 | handle : attr.TASK_MOVER_CLASS, 79 | placeholder: attr.TASK_PLACEHOLDER_CLASS, 80 | containment: view.domAttributes.PARENT_CONTAINER_CLASS, 81 | stop: function(event, ui) { 82 | /** 83 | * This is most likely the most complicated piece of code in `tyto`. 84 | * 85 | * It handles what happens when you move tasks from one column to 86 | * another. 87 | * 88 | * There may be a better way of doing this in a future release, but, 89 | * essentially we work out if the task is going to move column and if 90 | * it is we grab an instance of the view associated to the column. 91 | * 92 | * We then have to update the tasks' columnID, remove it from it's 93 | * current collection and add it to the new column collection. 94 | * 95 | * Lastly, we need to run our reordering logic to maintain ordinality 96 | * on page load. 97 | * 98 | * NOTE:: Also required to manually upgrade our MDL components here 99 | * after view/s have rendered. 100 | */ 101 | let list, destination; 102 | let model = view.collection.get(ui.item.attr(attr.TASK_ATTR)); 103 | let destinationView = view; 104 | let newColId = $(ui.item).parents(`[${attr.VIEW_ATTR}]`).attr(attr.VIEW_ATTR); 105 | if (newColId !== model.get('columnId')) { 106 | destination = Tyto.Columns.get(newColId); 107 | destinationView = Tyto.BoardView.children.findByModel(destination); 108 | list = destinationView.$el.find(attr.TASK_CLASS); 109 | model.save({ 110 | columnId: newColId 111 | }); 112 | view.collection.remove(model); 113 | destinationView.collection.add(model); 114 | Tyto.Utils.reorder(destinationView, list, attr.TASK_ATTR); 115 | destinationView.render(); 116 | destinationView.upgradeComponents(); 117 | } 118 | list = view.$el.find(attr.TASK_CLASS); 119 | Tyto.Utils.reorder(view, list, attr.TASK_ATTR); 120 | view.render(); 121 | view.upgradeComponents(); 122 | } 123 | }); 124 | }, 125 | onShow: function() { 126 | /** 127 | * If we are displaying a new column that will be rendered off the page 128 | * then we need to scroll over in order to see it when it is added. 129 | */ 130 | const view = this; 131 | const attr = view.domAttributes; 132 | const columns = $(attr.PARENT_CONTAINER_CLASS)[0]; 133 | const board = view.$el.parents(attr.BOARD_CLASS); 134 | if (columns.scrollWidth > window.outerWidth && board.hasClass(attr.COLUMN_ADD_CLASS)) { 135 | columns.scrollLeft = columns.scrollWidth; 136 | } 137 | view.upgradeComponents(); 138 | }, 139 | onRender: function() { 140 | this.bindTasks(); 141 | }, 142 | upgradeComponents: function() { 143 | const view = this; 144 | Tyto.Utils.upgradeMDL(view.getMDLMap()); 145 | }, 146 | updateTitle: function() { 147 | this.model.save({ 148 | title: this.ui.columnTitle.text() 149 | }); 150 | }, 151 | addTask: function() { 152 | const view = this; 153 | const attr = view.domAttributes; 154 | this.collection.add(Tyto.Tasks.create({ 155 | columnId: view.model.id, 156 | boardId : view.options.board.id, 157 | ordinal : view.collection.length + 1 158 | })); 159 | view.$el.addClass(attr.TASK_ADD_CLASS); 160 | }, 161 | deleteColumn: function() { 162 | if (this.collection.length === 0 || confirm(Tyto.CONFIRM_MESSAGE)) { 163 | while (this.collection.length !== 0) { 164 | this.collection.first().destroy(); 165 | } 166 | this.model.destroy(); 167 | } 168 | } 169 | }); 170 | 171 | export default ColumnView; 172 | -------------------------------------------------------------------------------- /src/script/views/board.js: -------------------------------------------------------------------------------- 1 | import Column from './column'; 2 | 3 | const BoardView = Backbone.Marionette.CompositeView.extend({ 4 | tagName: 'div', 5 | className: function() { 6 | return this.domAttributes.VIEW_CLASS; 7 | }, 8 | template: function(args) { 9 | return Tyto.TemplateStore.board(args); 10 | }, 11 | templateHelpers: function() { 12 | return { 13 | boards: Tyto.Boards 14 | }; 15 | }, 16 | childView: Column, 17 | childViewContainer: function() { 18 | return this.domAttributes.CHILD_VIEW_CONTAINER_CLASS; 19 | }, 20 | childViewOptions: function(c) { 21 | const view = this; 22 | const colTasks = Tyto.ActiveTasks.where({ 23 | columnId: c.id 24 | }); 25 | return { 26 | collection: new Tyto.Models.TaskCollection(colTasks), 27 | board: view.model 28 | }; 29 | }, 30 | ui: { 31 | addEntity : '.tyto-board__add-entity', 32 | primaryActions : '.tyto-board__actions', 33 | boardMenu : '.tyto-board__menu', 34 | boardSelect : '.tyto-board__selector__menu', 35 | addColumn : '.tyto-board__add-column', 36 | addTask : '.tyto-board__super-add', 37 | deleteBoard : '.tyto-board__delete-board', 38 | wipeBoard : '.tyto-board__wipe-board', 39 | emailBoard : '.tyto-board__email-board', 40 | emailer : '.tyto-board__emailer', 41 | boardName : '.tyto-board__title', 42 | columnContainer: '.tyto-board__columns', 43 | bloomer : '.tyto-board__bloomer' 44 | }, 45 | collectionEvents: { 46 | 'destroy': 'handleColumnRemoval' 47 | }, 48 | domAttributes: { 49 | VIEW_CLASS : 'tyto-board', 50 | CHILD_VIEW_CONTAINER_CLASS: '.tyto-board__columns', 51 | COLUMN_CLASS : '.tyto-column', 52 | COLUMN_ATTR : 'data-col-id', 53 | COLUMN_MOVER_CLASS : '.tyto-column__mover', 54 | COLUMN_PLACEHOLDER_CLASS : 'tyto-column__placeholder', 55 | FAB_MENU_VISIBLE_CLASS : 'is-showing-options', 56 | ADDING_COLUMN_CLASS : 'is--adding-column' 57 | }, 58 | getMDLMap: function() { 59 | const view = this; 60 | return [ 61 | { 62 | el: view.ui.boardMenu[0], 63 | component: 'MaterialMenu' 64 | }, { 65 | el: view.ui.boardSelect[0], 66 | component: 'MaterialMenu' 67 | } 68 | ]; 69 | }, 70 | handleColumnRemoval: function() { 71 | const view = this; 72 | const list = view.$el.find(view.domAttributes.COLUMN_CLASS); 73 | Tyto.Utils.reorder(view, list, view.domAttributes.COLUMN_ATTR); 74 | }, 75 | events: { 76 | 'click @ui.addEntity' : 'showPrimaryActions', 77 | 'click @ui.addColumn' : 'addNewColumn', 78 | 'click @ui.addTask' : 'addNewTask', 79 | 'click @ui.deleteBoard': 'deleteBoard', 80 | 'click @ui.wipeBoard' : 'wipeBoard', 81 | 'click @ui.emailBoard' : 'emailBoard', 82 | 'blur @ui.boardName' : 'saveBoardName' 83 | }, 84 | showPrimaryActions: function(e) { 85 | const view = this; 86 | const ctn = view.ui.primaryActions[0]; 87 | const btn = view.ui.addEntity[0]; 88 | const fabVisibleClass = view.domAttributes.FAB_MENU_VISIBLE_CLASS; 89 | const processClick = function(evt) { 90 | if (e.timeStamp !== evt.timeStamp) { 91 | ctn.classList.remove(fabVisibleClass); 92 | ctn.IS_SHOWING_MENU = false; 93 | document.removeEventListener('click', processClick); 94 | } 95 | }; 96 | if (!ctn.IS_SHOWING_MENU) { 97 | ctn.IS_SHOWING_MENU = true; 98 | ctn.classList.add(fabVisibleClass); 99 | document.addEventListener('click', processClick); 100 | } 101 | }, 102 | onBeforeRender: function() { 103 | this.collection.models = this.collection.sortBy('ordinal'); 104 | }, 105 | onShow: function() { 106 | /** 107 | * Have to upgrade MDL components onShow. 108 | */ 109 | const view = this; 110 | Tyto.Utils.upgradeMDL(view.getMDLMap()); 111 | }, 112 | onRender: function() { 113 | /** 114 | * As with manually upgrading MDL, need to invoke jQuery UI sortable 115 | * function on render. 116 | */ 117 | this.bindColumns(); 118 | }, 119 | bindColumns: function() { 120 | const view = this; 121 | const attr = view.domAttributes; 122 | view.ui.columnContainer.sortable({ 123 | connectWith: attr.COLUMN_CLASS, 124 | handle : attr.COLUMN_MOVER_CLASS, 125 | placeholder: attr.COLUMN_PLACEHOLDER_CLASS, 126 | axis : "x", 127 | containment: view.$childViewContainer, 128 | stop : function(event, ui) { 129 | const list = Array.prototype.slice.call(view.$el.find(attr.COLUMN_CLASS)); 130 | Tyto.Utils.reorder(view, list, attr.COLUMN_ATTR); 131 | } 132 | }); 133 | }, 134 | addNewColumn: function() { 135 | const view = this; 136 | const board = view.model; 137 | view.$el.addClass(view.domAttributes.ADDING_COLUMN_CLASS); 138 | const columns = view.collection; 139 | columns.add(Tyto.Columns.create({ 140 | boardId: board.id, 141 | ordinal: columns.length + 1 142 | })); 143 | }, 144 | saveBoardName: function() { 145 | this.model.save({ 146 | title: this.ui.boardName.text().trim() 147 | }); 148 | }, 149 | addNewTask: function() { 150 | const view = this; 151 | const board = view.model; 152 | const id = _.uniqueId(); 153 | const addUrl = `#board/${board.id}/task/${id}?isFresh=true`; 154 | Tyto.Utils.bloom(view.ui.addTask[0], Tyto.DEFAULT_TASK_COLOR, addUrl); 155 | }, 156 | deleteBoard: function() { 157 | const view = this; 158 | if (view.collection.length === 0 || confirm(Tyto.CONFIRM_MESSAGE)) { 159 | view.wipeBoard(); 160 | view.model.destroy(); 161 | view.destroy(); 162 | Tyto.navigate('/', { 163 | trigger: true 164 | }); 165 | } 166 | }, 167 | wipeBoard: function(dontConfirm) { 168 | const view = this; 169 | const wipe = function() { 170 | view.children.forEach(function(colView) { 171 | while (colView.collection.length !== 0) { 172 | colView.collection.first().destroy(); 173 | } 174 | colView.model.destroy(); 175 | }); 176 | }; 177 | if (dontConfirm) { 178 | if (confirm('[tyto] are you sure you wish to wipe the board?')) { 179 | wipe(); 180 | } 181 | } else { 182 | wipe(); 183 | } 184 | }, 185 | emailBoard: function() { 186 | const view = this; 187 | const emailContent = Tyto.Utils.getEmailContent(view.model); 188 | this.ui.emailer.attr('href', emailContent); 189 | this.ui.emailer[0].click(); 190 | } 191 | }); 192 | 193 | export default BoardView; 194 | -------------------------------------------------------------------------------- /src/script/views/edit.js: -------------------------------------------------------------------------------- 1 | const EditView = Backbone.Marionette.ItemView.extend({ 2 | template: function(args) { 3 | return Tyto.TemplateStore.edit(args); 4 | }, 5 | className: function() { 6 | return this.domAttributes.VIEW_CLASS; 7 | }, 8 | templateHelpers: function() { 9 | const view = this; 10 | return { 11 | selectedColumn: _.findWhere(view.options.columns, { 12 | id: view.model.get('columnId') 13 | }), 14 | board : this.options.board, 15 | columns: _.sortBy(this.options.columns, 'attributes.title'), 16 | isNew : this.options.isNew, 17 | colors : Tyto.TASK_COLORS 18 | }; 19 | }, 20 | domAttributes: { 21 | VIEW_CLASS : 'tyto-edit', 22 | BLOOM_SHOW_CLASS : 'is--showing-bloom', 23 | EDIT_SHOW_CLASS : 'is--showing-edit-view', 24 | MODEL_PROP_ATTR : 'data-model-prop', 25 | HIDDEN_UTIL_CLASS: 'is--hidden' 26 | }, 27 | props: { 28 | DEFAULT_COLOR_VALUE: 'default' 29 | }, 30 | ui: { 31 | save : '.tyto-edit__save', 32 | color : '.tyto-edit__color-select__menu-option', 33 | taskDescription: '.tyto-edit__task-description', 34 | editDescription: '.tyto-edit__edit-description', 35 | suggestions : '.tyto-task__suggestions', 36 | taskTitle : '.tyto-edit__task-title', 37 | column : '.tyto-edit__column-select__menu-option', 38 | colorMenu : '.tyto-edit__color-select__menu', 39 | columnMenu : '.tyto-edit__column-select__menu', 40 | columnLabel : '.tyto-edit__task-column', 41 | track : '.tyto-edit__track', 42 | time : '.tyto-edit__task-time', 43 | hours : '.tyto-edit__task-time__hours', 44 | minutes : '.tyto-edit__task-time__minutes' 45 | }, 46 | events: { 47 | 'click @ui.save' : 'saveTask', 48 | 'click @ui.color' : 'changeColor', 49 | 'click @ui.column' : 'changeColumn', 50 | 'click @ui.track' : 'trackTime', 51 | 'click @ui.taskDescription': 'showEditMode', 52 | 'blur @ui.editDescription' : 'updateTask', 53 | 'blur @ui.taskTitle' : 'updateTask', 54 | 'keydown @ui.taskTitle' : 'updateTask', 55 | 56 | /** 57 | * NOTE:: These are functions that are bootstrapped in from 58 | * the 'Suggestions' module. 59 | */ 60 | 'keypress @ui.editDescription': 'handleKeyInteraction', 61 | 'keydown @ui.editDescription' : 'handleKeyInteraction', 62 | 'keyup @ui.editDescription' : 'handleKeyInteraction', 63 | 'click @ui.suggestions' : 'selectSuggestion' 64 | }, 65 | initialize: function() { 66 | const view = this; 67 | Tyto.Suggestions.bootstrapView(view); 68 | Tyto.RootView.el.classList.add('bg--' + view.model.get('color')); 69 | Tyto.RootView.el.classList.remove(view.domAttributes.BLOOM_SHOW_CLASS); 70 | }, 71 | getMDLMap: function() { 72 | const view = this; 73 | return [ 74 | { 75 | el: view.ui.columnMenu[0], 76 | component: 'MaterialMenu' 77 | }, { 78 | el: view.ui.colorMenu[0], 79 | component: 'MaterialMenu' 80 | } 81 | ]; 82 | }, 83 | handleKeyInteraction: function(e) { 84 | if (e.which === 27) this.updateTask(e); 85 | this.filterItems(e); 86 | }, 87 | updateTask: function(e) { 88 | const view = this; 89 | const attr = view.domAttributes; 90 | const el = e.target; 91 | const val = (el.nodeName === 'TEXTAREA') ? el.value : el.innerHTML; 92 | view.model.set(el.getAttribute(attr.MODEL_PROP_ATTR), val); 93 | if (el.nodeName === 'TEXTAREA') { 94 | const desc = view.ui.taskDescription; 95 | const edit = view.ui.editDescription; 96 | desc.html(marked(edit.val())); 97 | edit.addClass(attr.HIDDEN_UTIL_CLASS); 98 | desc.removeClass(attr.HIDDEN_UTIL_CLASS); 99 | } 100 | if (e.type === 'keydown' && e.which === 27) 101 | this.ui.taskTitle.blur(); 102 | }, 103 | onShow: function() { 104 | Tyto.Utils.upgradeMDL(this.getMDLMap()); 105 | }, 106 | onRender: function() { 107 | const view = this; 108 | view.ui.taskDescription.html(marked(view.model.get('description'))); 109 | Tyto.Utils.autoSize(view.ui.editDescription[0]); 110 | Tyto.Utils.renderTime(view); 111 | }, 112 | trackTime: function() { 113 | Tyto.Utils.showTimeModal(this.model, this); 114 | }, 115 | showEditMode: function() { 116 | const domAttributes = this.domAttributes; 117 | const model = this.model; 118 | const desc = this.ui.taskDescription; 119 | const edit = this.ui.editDescription; 120 | desc.addClass(domAttributes.HIDDEN_UTIL_CLASS); edit.removeClass(domAttributes.HIDDEN_UTIL_CLASS) 121 | .val(model.get('description')) 122 | .focus(); 123 | }, 124 | 125 | /** 126 | * This is a function for handling fresh tasks and saving them on 'DONE' 127 | */ 128 | saveTask: function() { 129 | const view = this; 130 | const save = function() { 131 | delete view.model.attributes.id; 132 | Tyto.Tasks.create(view.model.attributes); 133 | Tyto.navigate('/board/' + view.options.board.id, true); 134 | }; 135 | if (view.options.columns.length !== 0 && !view.selectedColumnId) { 136 | alert('whoah, you need to select a column for that new task'); 137 | } else if (view.options.columns.length !== 0 && view.selectedColumnId) { 138 | save(); 139 | } else if (view.options.columns.length === 0) { 140 | const newCol = Tyto.Columns.create({ 141 | boardId: view.options.board.id, 142 | ordinal: 1 143 | }); 144 | view.model.set('columnId', newCol.id); 145 | view.model.set('ordinal', 1); 146 | save(); 147 | } 148 | }, 149 | changeColumn: function(e) { 150 | const view = this; 151 | const newColumnId = e.target.getAttribute('data-column-id'); 152 | if (newColumnId !== view.model.get('columnId')) { 153 | view.ui.column.removeClass(Tyto.SELECTED_CLASS); 154 | e.target.classList.add(Tyto.SELECTED_CLASS); 155 | const newOrdinal = Tyto.Tasks.where({ 156 | columnId: newColumnId 157 | }).length + 1; 158 | view.ui.columnLabel.text(e.target.textContent); 159 | view.selectedColumnId = newColumnId; 160 | view.model.set('columnId', newColumnId); 161 | view.model.set('ordinal', newOrdinal); 162 | } 163 | }, 164 | changeColor: function(e) { 165 | const view = this; 166 | const newColor = e.target.getAttribute('data-color'); 167 | Tyto.RootView.el.classList.add(view.domAttributes.EDIT_SHOW_CLASS); 168 | if (newColor !== view.props.DEFAULT_COLOR_VALUE) { 169 | view.ui.color.removeClass(Tyto.SELECTED_CLASS); 170 | e.target.classList.add(Tyto.SELECTED_CLASS); 171 | Tyto.RootView.el.classList.remove('bg--' + view.model.get('color')); 172 | Tyto.RootView.el.classList.add('bg--' + newColor); 173 | view.model.set('color', newColor); 174 | } 175 | }, 176 | onBeforeDestroy: function() { 177 | const view = this; 178 | Tyto.RootView.$el.removeClass('bg--' + view.model.get('color')); 179 | Tyto.RootView.$el.removeClass(view.domAttributes.EDIT_SHOW_CLASS); 180 | if (!view.options.isNew) { 181 | view.model.save(); 182 | } 183 | } 184 | }); 185 | 186 | export default EditView; 187 | -------------------------------------------------------------------------------- /src/script/utils/suggestions.js: -------------------------------------------------------------------------------- 1 | const Suggestions = function(Suggestions, App, Backbone, Marionette) { 2 | Suggestions.proto = [ 3 | 'filterItems', 4 | 'selectSuggestion', 5 | 'renderSuggestions', 6 | 'hideSuggestions' 7 | ]; 8 | Suggestions.props = { 9 | ACTIVE_CLASS: 'is--active', 10 | SUGGESTIONS_ITEM: '.tyto-suggestions__item' 11 | }; 12 | Suggestions.bootstrapView = function(view) { 13 | 14 | /* 15 | Bootstraps the given view with module functions. 16 | This is purely for a quick DRY fix. There is most definitely 17 | a better way to do this I am sure. 18 | */ 19 | Suggestions.proto.forEach(function(proto) { 20 | view[proto] = Suggestions[proto]; 21 | }); 22 | }; 23 | Suggestions.renderSuggestions = function(filterString) { 24 | const filterByTerm = function(entity) { 25 | return entity.attributes.title.toLowerCase().indexOf(filterString.toLowerCase()) !== -1; 26 | }; 27 | const view = this; 28 | const edit = view.ui.editDescription; 29 | const props = view.domAttributes; 30 | const suggestions = view.ui.suggestions; 31 | let collection = Tyto.Boards.models.concat(Tyto.Tasks.models); 32 | collection = (filterString) ? collection.filter(filterByTerm) : collection; 33 | const markup = Tyto.TemplateStore.filterList({ 34 | models: collection.slice(0, 4) 35 | }); 36 | const $body = $('body'); 37 | const $column = $('.tyto-column__tasks'); 38 | const end = edit[0].selectionEnd; 39 | const start = view.__EDIT_START + 1; 40 | const val = edit[0].value; 41 | const handleBlurring = function(e) { 42 | const el = e.target; 43 | if (el.nodeName !== 'LI' && el.nodeName !== 'TEXTAREA') { 44 | view.hideSuggestions(); 45 | view.delegateEvents(); 46 | edit.blur(); 47 | $body.off('click', handleBlurring); 48 | } else if (el.nodeName === 'TEXTAREA') { 49 | if (end < start || val.substring(start, end).indexOf(' ') !== -1) { 50 | view.hideSuggestions(); 51 | } 52 | } 53 | }; 54 | const scrollOff = function(e) { 55 | view.delegateEvents(); 56 | edit.focus(); 57 | $body.off('click', handleBlurring); 58 | $column.off('scroll', scrollOff); 59 | edit.off('scroll', scrollOff); 60 | view.hideSuggestions(); 61 | }; 62 | 63 | view.$el.off('blur', '.' + edit[0].className); 64 | $body.on('click', handleBlurring); 65 | $column.on('scroll', scrollOff); 66 | edit.on('scroll', scrollOff); 67 | if (!view.__EDIT_MODE) { 68 | view.__EDIT_MODE = true; 69 | view.__EDIT_START = edit[0].selectionStart; 70 | const coords = Tyto.Utils.getCaretPosition(edit[0]); 71 | suggestions.html(markup).css({ 72 | left: coords.LEFT, 73 | top: coords.TOP 74 | }).removeClass(props.HIDDEN_UTIL_CLASS); 75 | } else { 76 | suggestions.html(markup); 77 | } 78 | }; 79 | Suggestions.hideSuggestions = function() { 80 | const view = this; 81 | const props = view.domAttributes; 82 | view.__EDIT_MODE = false; 83 | view.__ACTIVE_SUGGESTION = null; 84 | view.__EDIT_MODE_IN_SELECTION = false; 85 | const suggestions = view.ui.suggestions; 86 | suggestions.addClass(props.HIDDEN_UTIL_CLASS); 87 | }; 88 | Suggestions.filterItems = function(e) { 89 | const view = this; 90 | const suggestions = view.ui.suggestions; 91 | const props = view.domAttributes; 92 | const edit = view.ui.editDescription; 93 | const key = e.which; 94 | const start = edit[0].selectionStart; 95 | const end = edit[0].selectionEnd; 96 | const val = edit[0].value; 97 | if (key === 35 && !view.__EDIT_MODE) { 98 | const before = val.charAt(start - 1).trim(); 99 | const after = val.charAt(start).trim(); 100 | if (before === '' && after === '') { 101 | view.renderSuggestions(); 102 | } 103 | } else if (view.__EDIT_MODE) { 104 | switch (key) { 105 | case 35: 106 | case 32: 107 | view.hideSuggestions(); 108 | case 13: 109 | if (view.__EDIT_MODE_IN_SELECTION && view.__ACTIVE_SUGGESTION !== null) { 110 | e.preventDefault(); 111 | view.__ACTIVE_SUGGESTION.click(); 112 | } else { 113 | view.hideSuggestions(); 114 | } 115 | break; 116 | case 8: 117 | if (end === view.__EDIT_START) { 118 | view.hideSuggestions(); 119 | } else { 120 | view.renderSuggestions(val.substring(view.__EDIT_START + 1, end)); 121 | } 122 | break; 123 | case 38: 124 | case 40: 125 | if (e.type === 'keydown') { 126 | e.preventDefault(); 127 | const dir = (key === 38) ? 'prev' : 'next'; 128 | const reset = (key === 38) ? 'last' : 'first'; 129 | if (view.__EDIT_MODE_IN_SELECTION) { 130 | if (view.__ACTIVE_SUGGESTION[dir]().length === 0) { 131 | view.__ACTIVE_SUGGESTION.removeClass(Suggestions.props.ACTIVE_CLASS); 132 | view.__ACTIVE_SUGGESTION = suggestions.find(Suggestions.props.SUGGESTIONS_ITEM)[reset]().addClass(Suggestions.props.ACTIVE_CLASS); 133 | } else { 134 | view.__ACTIVE_SUGGESTION = view.__ACTIVE_SUGGESTION.removeClass(Suggestions.props.ACTIVE_CLASS)[dir]().addClass(Suggestions.props.ACTIVE_CLASS); 135 | } 136 | } else { 137 | view.__EDIT_MODE_IN_SELECTION = true; 138 | view.__ACTIVE_SUGGESTION = suggestions.find(Suggestions.props.SUGGESTIONS_ITEM)[reset]().addClass(Suggestions.props.ACTIVE_CLASS); 139 | } 140 | } 141 | break; 142 | case 37: 143 | case 39: 144 | if (e.type === 'keyup') { 145 | if (end < (view.__EDIT_START + 1) || val.substring(view.__EDIT_START, end).length !== val.substring(view.__EDIT_START, end).trim().length) { 146 | view.hideSuggestions(); 147 | } 148 | } 149 | break; 150 | default: 151 | if (e.type === 'keyup') { 152 | view.renderSuggestions(val.substring(view.__EDIT_START + 1, end)); 153 | } 154 | } 155 | } 156 | }; 157 | Suggestions.selectSuggestion = function(e) { 158 | const view = this; 159 | const edit = view.ui.editDescription; 160 | const entityType = e.target.getAttribute('data-type'); 161 | const entityId = e.target.getAttribute('data-model-id'); 162 | if (entityType) { 163 | const entity = Tyto[entityType].get(entityId); 164 | let url; 165 | if (entity.attributes.boardId) { 166 | const boardId = Tyto.Tasks.get(entityId).attributes.boardId; 167 | url = '#board/' + boardId + '/task/' + entityId; 168 | } else { 169 | url = '#board/' + entityId; 170 | } 171 | url = '[' + entity.attributes.title + '](' + url + ')'; 172 | const start = edit[0].value.slice(0, view.__EDIT_START); 173 | const end = edit[0].value.slice(edit[0].selectionEnd, edit[0].value.length); 174 | edit[0].value = start + ' ' + url + ' ' + end; 175 | } 176 | $('body').off('click'); 177 | view.ui.editDescription.focus(); 178 | view.hideSuggestions(); 179 | view.delegateEvents(); 180 | }; 181 | }; 182 | 183 | export default Suggestions; 184 | -------------------------------------------------------------------------------- /test/test.js: -------------------------------------------------------------------------------- 1 | describe('tyto', function() { 2 | var boardModel, 3 | boardsCollection, 4 | columnsCollection, 5 | tasksCollection, 6 | taskModel, 7 | columnModel, 8 | testView; 9 | describe('Views', function() { 10 | beforeEach(function() { 11 | boardsCollection = new Tyto.Models.BoardCollection(); 12 | boardModel = boardsCollection.create(); 13 | }); 14 | describe('BoardView', function() { 15 | beforeEach(function(){ 16 | columnsCollection = new Tyto.Models.ColumnCollection(); 17 | viewCollection = new Tyto.Models.ColumnCollection(); 18 | testView = new Tyto.Views.Board({ 19 | model : boardModel, 20 | collection: viewCollection 21 | }); 22 | testView.render(); 23 | }); 24 | it('Using the delete option destroys the model', function() { 25 | testView.ui.deleteBoard.click(); 26 | expect(boardsCollection.length).to.equal(0); 27 | }); 28 | it('Using the wipe board option, destroys all models associated with a board', function() { 29 | /* 30 | Test requires having at least a column and a task defined 31 | for the board. 32 | */ 33 | testView.collection.add(columnsCollection.create()); 34 | colView = testView.children.first(); 35 | colView.ui.addTask.click(); 36 | /* 37 | Check here that correct amount of tasks has been created. 38 | 39 | NOTE:: 2 is the correct amount as ui.addTask refers to 2 40 | element that will both be clicked on invoking .click() 41 | */ 42 | expect(colView.collection.length).to.equal(2); 43 | /* 44 | wipeBoard uses a "confirm" to verify that you want to delete the 45 | board contents. 46 | 47 | Instead of testing the functionality on click. We can test the 48 | view function by invoking it direct. 49 | */ 50 | testView.wipeBoard(); 51 | expect(colView.collection.length).to.equal(0); 52 | expect(testView.collection.length).to.equal(0); 53 | 54 | }); 55 | it('Renders the correct amount of columns', function() { 56 | expect(testView.$el.find('.tyto-column').length).to.equal(0); 57 | testView.collection.add(columnsCollection.create()); 58 | expect(testView.$el.find('.tyto-column').length).to.equal(1); 59 | testView.collection.add(columnsCollection.create()); 60 | expect(testView.$el.find('.tyto-column').length).to.equal(2); 61 | }); 62 | it('Clicking addColumn btn increases collection', function() { 63 | testView.ui.addColumn.click(); 64 | expect(testView.collection.length).to.equal(1); 65 | }); 66 | }); 67 | describe('ColumnView', function() { 68 | beforeEach(function() { 69 | columnsCollection = new Tyto.Models.ColumnCollection(); 70 | taskCollection = new Tyto.Models.TaskCollection(); 71 | 72 | columnModel = columnsCollection.create({ 73 | boardId: boardModel.id, 74 | ordinal: columnsCollection.length + 1 75 | }); 76 | 77 | viewCollection = new Tyto.Models.TaskCollection(); 78 | testView = new Tyto.Views.Column({ 79 | model : columnModel, 80 | collection: viewCollection, 81 | board : boardModel 82 | }); 83 | testView.render(); 84 | }); 85 | it('Renders the correct amount of tasks', function() { 86 | expect(testView.$el.find('.tyto-task').length).to.equal(0); 87 | testView.collection.add(taskCollection.create()); 88 | expect(testView.$el.find('.tyto-task').length).to.equal(1); 89 | testView.collection.add(taskCollection.create()); 90 | expect(testView.$el.find('.tyto-task').length).to.equal(2); 91 | }); 92 | it('Using the delete option destroys the model', function(){ 93 | testView.ui.deleteColumn.click(); 94 | expect(columnsCollection.length).to.equal(0); 95 | }); 96 | it('Using the add task UI components adds tasks', function() { 97 | testView.ui.addTask.click(); 98 | /* 99 | The length expected here should actually be 2 and not 1 as there 100 | are two buttons for adding tasks both referenced by the same className 101 | 102 | Firing a click, will fire on both, essentially adding 2 tasks. 103 | */ 104 | expect(viewCollection.length).to.equal(2); 105 | }); 106 | }); 107 | describe('TaskView', function() { 108 | beforeEach(function() { 109 | tasksCollection = new Tyto.Models.TaskCollection(); 110 | }); 111 | it('Quick edit of title/description updates model', function() { 112 | taskModel = tasksCollection.create(); 113 | testView = new Tyto.Views.Task( 114 | { 115 | model: taskModel 116 | } 117 | ); 118 | testView.render(); 119 | var UPDATED_TITLE = 'Updated title', 120 | UPDATED_DESCRIPTION = 'Updated description'; 121 | // Test out the title field 122 | testView.ui.title.text(UPDATED_TITLE); 123 | testView.ui.title.blur(); 124 | expect(testView.model.get('title')).to.equal(UPDATED_TITLE); 125 | // Test out the description field 126 | testView.ui.editDescription.text(UPDATED_DESCRIPTION); 127 | testView.ui.editDescription.blur(); 128 | expect(testView.model.get('description')).to.equal(UPDATED_DESCRIPTION); 129 | }); 130 | it('Honors markdown support by rendering correct output', function(){ 131 | var description = '__bold__', 132 | UPDATED_DESCRIPTION = 'This is in _italics_', 133 | mdOutput = marked(description); 134 | taskModel = tasksCollection.create(); 135 | taskModel.set('description', description); 136 | testView = new Tyto.Views.Task( 137 | { 138 | model: taskModel 139 | } 140 | ); 141 | testView.render(); 142 | expect(testView.ui.description.html()).to.equal(mdOutput); 143 | mdOutput = marked(UPDATED_DESCRIPTION); 144 | testView.ui.editDescription.text(UPDATED_DESCRIPTION); 145 | testView.ui.editDescription.blur(); 146 | expect(testView.ui.description.html()).to.equal(mdOutput); 147 | }); 148 | }); 149 | describe('EditView', function() { 150 | beforeEach(function() { 151 | taskModel = new Tyto.Models.Task({ 152 | boardId: boardModel.id, 153 | id : _.uniqueId() 154 | }); 155 | }); 156 | it('Should update correct model property on blur', function() { 157 | testView = new Tyto.Views.Edit({ 158 | model : taskModel, 159 | board : boardModel, 160 | columns: [], 161 | isNew : true 162 | }); 163 | testView.render(); 164 | // Test out the title field 165 | testView.ui.taskTitle.text('Updated title'); 166 | testView.ui.taskTitle.blur(); 167 | expect(testView.model.get('title')).to.equal('Updated title'); 168 | // Test out the description field 169 | testView.ui.editDescription.text('Updated description'); 170 | testView.ui.editDescription.blur(); 171 | expect(testView.model.get('description')).to.equal('Updated description'); 172 | }); 173 | it('Honors markdown support by rendering correct output', function(){ 174 | var description = '__bold__', 175 | UPDATED_DESCRIPTION = 'This is in _italics_', 176 | mdOutput = marked(description); 177 | taskModel = tasksCollection.create(); 178 | taskModel.set('description', description); 179 | testView = new Tyto.Views.Edit( 180 | { 181 | model : taskModel, 182 | board : boardModel, 183 | columns: [], 184 | isNew : true 185 | } 186 | ); 187 | testView.render(); 188 | expect(testView.ui.taskDescription.html()).to.equal(mdOutput); 189 | mdOutput = marked(UPDATED_DESCRIPTION); 190 | testView.ui.editDescription.text(UPDATED_DESCRIPTION); 191 | testView.ui.editDescription.blur(); 192 | expect(testView.ui.taskDescription.html()).to.equal(mdOutput); 193 | }); 194 | }); 195 | }); 196 | }); 197 | -------------------------------------------------------------------------------- /src/script/utils/utils.js: -------------------------------------------------------------------------------- 1 | const Utils = function(Utils, App, Backbone, Marionette) { 2 | Utils.upgradeMDL = function(map) { 3 | _.forEach(map, function(upgrade, idx) { 4 | if (upgrade.el) { 5 | componentHandler.upgradeElement(upgrade.el, upgrade.component); 6 | } 7 | }); 8 | }; 9 | 10 | /* 11 | Syncs model 'ordinal' property to that of the DOM representation. 12 | 13 | NOTE :: This shouldn't be doing a loop through the collection using 14 | model.save. With a proper backend this could be avoided but on 15 | localStorage it will work with no real performance hit. 16 | */ 17 | Utils.reorder = function(entity, list, attr) { 18 | const collection = entity.collection; 19 | _.forEach(list, function(item, idx) { 20 | const id = item.getAttribute(attr); 21 | const model = collection.get(id); 22 | if (model) { 23 | model.save({ 24 | ordinal: idx + 1 25 | }); 26 | } 27 | }); 28 | }; 29 | Utils.autoSize = function(el) { 30 | const sizeUp = function() { 31 | el.style.height = 'auto'; 32 | el.style.height = el.scrollHeight + 'px'; 33 | }; 34 | el.addEventListener('keydown', sizeUp); 35 | el.addEventListener('input', sizeUp); 36 | el.addEventListener('focus', sizeUp); 37 | sizeUp(); 38 | }; 39 | Utils.getCaretPosition = function(el) { 40 | const carPos = el.selectionEnd; 41 | const div = document.createElement('div'); 42 | const span = document.createElement('span'); 43 | const copyStyle = getComputedStyle(el); 44 | const bounds = el.getBoundingClientRect(); 45 | [].forEach.call(copyStyle, function(prop) { 46 | return div.style[prop] = copyStyle[prop]; 47 | }); 48 | div.style.position = 'absolute'; 49 | div.textContent = el.value.substr(0, carPos); 50 | span.textContent = el.value.substr(carPos) || '.'; 51 | div.appendChild(span); 52 | document.body.appendChild(div); 53 | const fontSize = parseFloat(copyStyle.fontSize.replace('px', ''), 10); 54 | let top = el.offsetTop - el.scrollTop + span.offsetTop + fontSize; 55 | top = (top > el.offsetHeight) ? el.offsetHeight : top; 56 | const left = el.offsetLeft - el.scrollLeft + span.offsetLeft; 57 | const coords = { 58 | TOP: top + bounds.top + 'px', 59 | LEFT: left + bounds.left + 'px' 60 | }; 61 | document.body.removeChild(div); 62 | return coords; 63 | }; 64 | Utils.processQueryString = function(params) { 65 | const qS = {}; 66 | const pushToQs = function(set) { 67 | set = set.split('='); 68 | qS[set[0]] = set[1]; 69 | }; 70 | params.split('&').map(pushToQs); 71 | return qS; 72 | }; 73 | Utils.bloom = function(el, color, url) { 74 | const $bloomer = Tyto.BoardView.ui.bloomer; 75 | const bloomer = $bloomer[0]; 76 | const coord = el.getBoundingClientRect(); 77 | bloomer.style.left = coord.left + (coord.width / 2) + 'px'; 78 | bloomer.style.top = coord.top + (coord.height / 2) + 'px'; 79 | bloomer.className = 'tyto-board__bloomer ' + 'bg--' + color; 80 | bloomer.classList.add('is--blooming'); 81 | Tyto.RootView.el.classList.add('is--showing-bloom'); 82 | const goToEdit = function() { 83 | $bloomer.off(Tyto.ANIMATION_EVENT, goToEdit); 84 | Tyto.navigate(url, true); 85 | }; 86 | $bloomer.on(Tyto.ANIMATION_EVENT, goToEdit); 87 | }; 88 | Utils.load = function(data, importing, wipe) { 89 | const boards = []; 90 | const cols = []; 91 | const tasks = []; 92 | const altered = {}; 93 | if (importing) { 94 | delete data.tyto; 95 | delete data['tyto--board']; 96 | delete data['tyto--column']; 97 | delete data['tyto--task']; 98 | } 99 | if (wipe) { 100 | _.forOwn(window.localStorage, function(val, key) { 101 | if (key.indexOf('tyto') !== -1) { 102 | window.localStorage.removeItem(key); 103 | } 104 | }); 105 | } 106 | _.forOwn(data, function(val, key) { 107 | let entity, saveId; 108 | if (wipe) { 109 | window.localStorage.setItem(key, val); 110 | } 111 | if (key.indexOf('tyto--board-') !== -1) { 112 | if (importing) { 113 | entity = JSON.parse(val); 114 | if (Tyto.Boards.get(entity.id) !== undefined) { 115 | saveId = entity.id; 116 | delete entity.id; 117 | } 118 | altered[saveId] = Tyto.Boards.create(entity).id; 119 | } else { 120 | boards.push(JSON.parse(val)); 121 | } 122 | } 123 | if (key.indexOf('tyto--column-') !== -1) { 124 | if (importing) { 125 | entity = JSON.parse(val); 126 | if (altered[entity.boardId]) { 127 | entity.boardId = altered[entity.boardId]; 128 | } 129 | if (Tyto.Columns.get(entity.id) !== undefined) { 130 | saveId = entity.id; 131 | delete entity.id; 132 | } 133 | altered[saveId] = Tyto.Columns.create(entity).id; 134 | } else { 135 | cols.push(JSON.parse(val)); 136 | } 137 | } 138 | if (key.indexOf('tyto--task-') !== -1) { 139 | if (importing) { 140 | entity = JSON.parse(val); 141 | if (altered[entity.boardId]) { 142 | entity.boardId = altered[entity.boardId]; 143 | } 144 | if (altered[entity.columnId]) { 145 | entity.columnId = altered[entity.columnId]; 146 | } 147 | if (Tyto.Tasks.get(entity.id) !== undefined) { 148 | saveId = entity.id; 149 | delete entity.id; 150 | } 151 | altered[saveId] = Tyto.Tasks.create(entity).id; 152 | } else { 153 | tasks.push(JSON.parse(val)); 154 | } 155 | } 156 | }); 157 | if (!importing) { 158 | Tyto.Boards.reset(boards); 159 | Tyto.Columns.reset(cols); 160 | Tyto.Tasks.reset(tasks); 161 | } 162 | }; 163 | 164 | /** 165 | * ES6 to the rescue!!! 166 | */ 167 | Utils.EMAIL_TEMPLATE = ` 168 |
169 | Status for: <%= board.title %> 170 | <% if (columns.length > 0 && tasks.length > 0) { %> 171 | <% _.forEach(columns, function(column) { %> 172 | <%= column.attributes.title %> 173 | —————————— 174 | <% _.forEach(tasks, function(task) { %> 175 | <% if (task.attributes.columnId === column.attributes.id) { %> 176 | • <%= task.attributes.title %> 177 | \n 178 | <%= task.attributes.description %> 179 | <% if (task.attributes.timeSpent.hours > 0 || task.attributes.timeSpent.minutes > 0) { %> 180 | \n 181 | -- <%=task.attributes.timeSpent.hours %> hours, <%= task.attributes.timeSpent.minutes %> minutes. 182 | <% } %> 183 | <% } %> 184 | <% });%> 185 | <% }); %> 186 | <% } else { %> 187 | Seems we are way ahead, so treat yourself and go grab a coffee! :) 188 | <% } %> 189 |
`; 190 | Utils.getEmailContent = function(board) { 191 | const subject = `Status for ${Tyto.ActiveBoard.get('title')} as of ${new Date().toString()}`; 192 | const templateFn = _.template(Tyto.Utils.EMAIL_TEMPLATE); 193 | let content = templateFn({ 194 | board: board.attributes, 195 | columns: Tyto.Columns.where({ 196 | boardId: board.id 197 | }), 198 | tasks: Tyto.Tasks.where({ 199 | boardId: board.id 200 | }) 201 | }); 202 | content = encodeURIComponent($(content).text()); 203 | return `mailto:someone@somewhere.com?subject=${encodeURIComponent(subject.trim())}&body=${content}`; 204 | }; 205 | Utils.showTimeModal = function(model, view) { 206 | Tyto.RootView.$el.prepend($('
')); 207 | Tyto.RootView.addRegion('TimeModal', '.tyto-time-modal__wrapper'); 208 | Tyto.TimeModalView = new App.Views.TimeModal({ 209 | model : model, 210 | modelView: view 211 | }); 212 | Tyto.RootView.showChildView('TimeModal', Tyto.TimeModalView); 213 | }; 214 | Utils.getRenderFriendlyTime = function(time) { 215 | const renderTime = {}; 216 | for(let measure of ['hours', 'minutes', 'seconds']) { 217 | renderTime[measure] = (time[measure] < 10) ? '0' + time[measure] : time[measure]; 218 | } 219 | return renderTime; 220 | }; 221 | return Utils.renderTime = function(view) { 222 | const time = view.model.get('timeSpent'); 223 | if (time.hours > 0 || time.minutes > 0) { 224 | if (view.ui.time.hasClass(view.domAttributes.HIDDEN_UTIL_CLASS)) { 225 | view.ui.time.removeClass(view.domAttributes.HIDDEN_UTIL_CLASS); 226 | } 227 | const friendly = Tyto.Utils.getRenderFriendlyTime(time); 228 | view.ui.hours.text(friendly.hours + 'h'); 229 | view.ui.minutes.text(friendly.minutes + 'm'); 230 | } else { 231 | if (!view.ui.time.hasClass(view.domAttributes.HIDDEN_UTIL_CLASS)) { 232 | view.ui.time.addClass(view.domAttributes.HIDDEN_UTIL_CLASS); 233 | } 234 | } 235 | }; 236 | }; 237 | 238 | export default Utils; 239 | -------------------------------------------------------------------------------- /src/script/templates/templates.js: -------------------------------------------------------------------------------- 1 | module.exports = { "board": function(tyto) { 2 | var __t, __p = '', __j = Array.prototype.join; 3 | function print() { __p += __j.call(arguments, '') } 4 | __p += '

' + 5 | ((__t = ( tyto.title )) == null ? '' : __t) + 6 | '

'; 7 | if (tyto.boards.length > 1) { ; 8 | __p += ''; 21 | } ; 22 | __p += '
'; 23 | return __p 24 | },"column": function(tyto) { 25 | var __t, __p = ''; 26 | __p += '
open_with
' + 27 | ((__t = ( tyto.title )) == null ? '' : __t) + 28 | '
  • Delete
  • Add task
add
'; 33 | return __p 34 | },"cookieBanner": function(tyto) { 35 | var __t, __p = ''; 36 | __p += '

tyto uses cookies that enable it to provide functionality and a better user experience. By using tyto and closing this message you agree to the use of cookies. Read more...

'; 37 | return __p 38 | },"edit": function(tyto) { 39 | var __t, __p = '', __j = Array.prototype.join; 40 | function print() { __p += __j.call(arguments, '') } 41 | __p += '
'; 42 | if (tyto.isNew) { ; 43 | __p += 'Cancel'; 46 | } else { ; 47 | __p += 'Return to board'; 50 | } ; 51 | __p += '

' + 52 | ((__t = ( tyto.title )) == null ? '' : __t) + 53 | '

' + 54 | ((__t = ( tyto.description )) == null ? '' : __t) + 55 | '
'; 66 | if (tyto.columns.length > 0 ) { ; 67 | __p += '
    '; 68 | _.forEach(tyto.columns, function(column) { ; 69 | __p += '\n'; 70 | if (!tyto.isNew) { var activeClass = (column.attributes.id === tyto.columnId) ? 'is--selected': '' } ; 71 | __p += '
  • ' + 76 | ((__t = ( column.attributes.title )) == null ? '' : __t) + 77 | '
  • '; 78 | }); ; 79 | __p += '
'; 80 | } ; 81 | __p += '
    '; 82 | _.forEach(tyto.colors, function(col) { ; 83 | __p += '\n'; 84 | var activeColor = (col === tyto.color) ? 'is--selected': '' ; 85 | __p += '
  • '; 96 | }); ; 97 | __p += '
'; 98 | return __p 99 | },"filterList": function(tyto) { 100 | var __t, __p = '', __j = Array.prototype.join; 101 | function print() { __p += __j.call(arguments, '') } 102 | __p += ''; 127 | return __p 128 | },"menu": function(tyto) { 129 | var __t, __p = ''; 130 | __p += '

tyto

'; 131 | return __p 132 | },"select": function(tyto) { 133 | var __t, __p = '', __j = Array.prototype.join; 134 | function print() { __p += __j.call(arguments, '') } 135 | 136 | if (!tyto.items || tyto.items.length == 0) { ; 137 | __p += '

To start,

or

'; 138 | } else {; 139 | __p += '

To start, select one of your boards.

Alternatively,

'; 148 | } ; 149 | 150 | return __p 151 | },"task": function(tyto) { 152 | var __t, __p = '', __j = Array.prototype.join; 153 | function print() { __p += __j.call(arguments, '') } 154 | __p += '
open_with

' + 155 | ((__t = ( tyto.title )) == null ? '' : __t) + 156 | '

  • Delete
  • Edit
  • Track
' + 161 | ((__t = ( tyto.description )) == null ? '' : __t) + 162 | '
'; 163 | var hidden = (tyto.timeSpent.hours > 0 || tyto.timeSpent.minutes > 0) ? '': 'is--hidden'; ; 164 | __p += '
schedule' + 165 | ((__t = ( tyto.timeSpent.hours )) == null ? '' : __t) + 166 | 'h' + 167 | ((__t = ( tyto.timeSpent.minutes )) == null ? '' : __t) + 168 | 'm
'; 169 | return __p 170 | },"timeModal": function(tyto) { 171 | var __t, __p = ''; 172 | __p += '

' + 173 | ((__t = ( tyto.title )) == null ? '' : __t) + 174 | '

::

'; 175 | return __p 176 | } }; --------------------------------------------------------------------------------