├── Procfile ├── .gitignore ├── public ├── config.js ├── assets │ └── css │ │ ├── style.css │ │ ├── editor.css │ │ └── simplex.bootstrap.min.css ├── test │ ├── index.html │ └── sorter-spec.js ├── lib │ ├── login.service.js │ └── drive.service.js ├── index.html ├── sorter.js └── main.js ├── server.js ├── package.json ├── LICENSE └── readme.md /Procfile: -------------------------------------------------------------------------------- 1 | web: node server.js 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | npm-debug.log 4 | -------------------------------------------------------------------------------- /public/config.js: -------------------------------------------------------------------------------- 1 | var config = { 2 | SCOPES: 'https://www.googleapis.com/auth/drive.file', 3 | DISCOVERY_DOCS: ['https://www.googleapis.com/discovery/v1/apis/drive/v3/rest'], 4 | CLIENT_ID: '' 5 | } 6 | -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | var express = require('express'); 2 | var app = express(); 3 | var fs = require('fs'); 4 | 5 | app.use(express.static(__dirname + '/public')); 6 | 7 | app.listen(process.env.PORT || 4000); 8 | 9 | module.exports = {app} 10 | -------------------------------------------------------------------------------- /public/assets/css/style.css: -------------------------------------------------------------------------------- 1 | #container-header h1 { 2 | margin: 20px 0px; 3 | display: inline-block; 4 | } 5 | #container-header span { 6 | margin-left: 70%; 7 | } 8 | 9 | #menu .panel-small { 10 | height: 200px; 11 | overflow: auto; 12 | } 13 | 14 | .sub_bookmark_link { 15 | margin-left: 10px; 16 | } 17 | 18 | .not_authenticated, .authenticated { 19 | display: none; 20 | } 21 | 22 | @media (min-width: 1200px) { 23 | .container { 24 | width: 85%; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Sorter", 3 | "version": "1.0.0", 4 | "description": "Sorter is a webapp to organize ideas, tasks and information using bullet points and hashtags.", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "node server.js", 8 | "dev": "nodemon server.js -e js,html", 9 | "test": "./node_modules/mocha/bin/mocha tests" 10 | }, 11 | "dependencies": { 12 | "express": "^4.14.0" 13 | }, 14 | "repository": { 15 | "type": "git", 16 | "url": "" 17 | }, 18 | "engines": { 19 | "node": "6.4.x", 20 | "npm": "3.10.x" 21 | }, 22 | "keywords": [ 23 | "google drive api v3", 24 | "Javascript", 25 | "Sorter", 26 | "Javascript", 27 | "organize", 28 | "webapp" 29 | ], 30 | "author": "vitogit", 31 | "license": "MIT" 32 | } 33 | -------------------------------------------------------------------------------- /public/test/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 |
14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /public/assets/css/editor.css: -------------------------------------------------------------------------------- 1 | ul, 2 | li { 3 | padding: 0; 4 | list-style: none; 5 | background-color: #fcfcfc; 6 | } 7 | 8 | li { 9 | margin: 0 0 0 1em; 10 | } 11 | 12 | li:before { 13 | content: '□'; 14 | font-family: 'FontAwesome'; 15 | float: left; 16 | margin-left: -1em; 17 | padding-right: 5px; 18 | } 19 | 20 | li.leaf:before { 21 | content: '■'; 22 | color: #d9230f; 23 | } 24 | 25 | li.completed:before{ 26 | content: '✔'; 27 | color: #4caf50!important; 28 | } 29 | 30 | li.journal:before { 31 | content: '★'; 32 | color: #029acf; 33 | } 34 | 35 | li.todo:before { 36 | color: #ffeb3b; 37 | } 38 | 39 | li.closed-icon ul { 40 | /*display:none;*/ 41 | } 42 | 43 | li.closed-icon:before { 44 | content: '▸'; 45 | } 46 | 47 | li.filtered { 48 | background-color: rgba(0,0,0,0.075); 49 | } 50 | li:not(.filtered):not(.smartTag) { 51 | background-color: #fcfcfc; 52 | } 53 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 vitogit (Alvaro) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /public/lib/login.service.js: -------------------------------------------------------------------------------- 1 | function LoginService(clientId, scopes, discoveryDocs){ 2 | this.initClient = function(updateSigninStatus) { 3 | 4 | return gapi.client.init({ 5 | clientId: clientId, 6 | scope: scopes, 7 | discoveryDocs: discoveryDocs 8 | }).then(function (resp) { 9 | // Listen for sign-in state changes. 10 | gapi.auth2.getAuthInstance().isSignedIn.listen(updateSigninStatus); 11 | // Handle the initial sign-in state. 12 | updateSigninStatus(gapi.auth2.getAuthInstance().isSignedIn.get()); 13 | }, function(reason){ 14 | console.log('ERROR: '+reason) 15 | }); 16 | } 17 | 18 | this.signIn = function() { 19 | return gapi.auth2.getAuthInstance().signIn(); 20 | } 21 | 22 | this.signOut = function() { 23 | return gapi.auth2.getAuthInstance().signOut(); 24 | } 25 | 26 | //return an object and you can call: getId() getName(), getEmail() 27 | //ref: https://developers.google.com/identity/sign-in/web/reference#googleusergetbasicprofile 28 | this.userProfile = function() { 29 | return gapi.auth2.getAuthInstance().currentUser.get().getBasicProfile() 30 | } 31 | 32 | 33 | 34 | } 35 | -------------------------------------------------------------------------------- /public/lib/drive.service.js: -------------------------------------------------------------------------------- 1 | function DriveService(){ 2 | 3 | this.loadFile = function(file, done) { 4 | gapi.client.drive.files.export({ 5 | fileId: file.id, 6 | mimeType: 'text/plain', 7 | fields: 'id,name' 8 | }).then(function(resp) { 9 | var retFile = {name: file.name, id: file.id, content: resp.body} 10 | done(retFile) 11 | }); 12 | } 13 | 14 | this.saveFile = function(file, done) { 15 | function addContent(fileId) { 16 | return gapi.client.request({ 17 | path: '/upload/drive/v3/files/' + fileId, 18 | method: 'PATCH', 19 | params: { 20 | uploadType: 'media' 21 | }, 22 | body: file.content 23 | }) 24 | } 25 | 26 | if (file.id) { //just update 27 | addContent(file.id).then(function(resp) { 28 | console.log('File just updated', resp.result) 29 | done(resp.result) 30 | }) 31 | } else { //create and update 32 | gapi.client.drive.files.create({ 33 | mimeType: 'application/vnd.google-apps.document', 34 | name: file.name, 35 | fields: 'id' 36 | }).then(function(resp) { 37 | addContent(resp.result.id).then(function(resp) { 38 | console.log('created and added content', resp.result) 39 | done(resp.result) 40 | }) 41 | }); 42 | } 43 | } 44 | 45 | this.listFiles = function(done) { 46 | gapi.client.drive.files.list({ 47 | pageSize: 30, 48 | corpus: 'user', 49 | spaces: 'drive', 50 | fields: "nextPageToken, files(id, name)", 51 | q: 'name contains "sorter_"', 52 | orderBy: 'modifiedTime desc' 53 | }).then(function(resp) { 54 | return done(null, resp.result.files) 55 | },function(reason) { 56 | return done(reason, null) 57 | }) 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # Sorter 2 | 3 | Sorter is a webapp to organize ideas, tasks and information using bullet points and hashtags. 4 | 5 | ## How it works 6 | 7 | #### Write your notes following a hierarchy with bullets points 8 | ![Sorter in action](https://user-images.githubusercontent.com/5280619/66678930-5dab9280-ec43-11e9-833f-e87dbbcfbd15.png) 9 | 10 | #### Add hashtags to your notes and filter by hashtags or words to just focus on what you need 11 | ![Sorter with tags](https://user-images.githubusercontent.com/5280619/66678925-5ab0a200-ec43-11e9-8d21-aee7658b1084.png) 12 | 13 | If you write `&` anywhere in the filter it will perform an AND filter: 14 | `ex: & #task $todo it will search notes that have both hashtags.` 15 | in other case it will perform and OR filter. 16 | `ex: #task $todo it will search notes that have any of the hashtags.` 17 | 18 | 19 | #### Sync your info with Google Drive. 20 | Click the save button and it will create a new document in your google drive called: sorter_notes. It will be loaded automatically when the app start. 21 | It uses my library gSyncDrive for sync with Google Drive Api v3 https://github.com/vitogit/gDriveSync.js 22 | 23 | #### Suggested hashtags 24 | 25 | - read-later: something that you want to read later 26 | - bookmark: articles or internet links 27 | - tip: something interesting that you found 28 | - goal: maybe a long term goal with a lot of actions and sub goals 29 | - action: something that you have to do, some task, call, etc 30 | - idea: ideas to develop in the future 31 | - p1, p2, p3: add priority to your tasks so then you can filter important tasks 32 | 33 | ##### SmartTags: they are a special kind of hashtag with buildin functionality: 34 | - $todo : the note is painted yellow 35 | - $completed: the note is painted green 36 | - $journal: the current date is added and so you could filter by date. [not implemented yet] 37 | - For default journal and completed task are hidden, you can uncheck them in the sidebar. 38 | 39 | ##### Notebooks 40 | You can create new notebooks (google docs) and access them easily from the left menu. Just write the name and click "save new" 41 | There is a default notebook called `home`, all notebooks has a prefix: sorter_notes_ so that's is how they look in google drive. 42 | 43 | Also you can add @my_notebook to your notes and when clicked it will redirect to the notebook called my_notebook 44 | 45 | ##### Writing notes and taking shortcuts 46 | Make sure that you are always writing bullet points. Click in the bullet list icon if is not set. 47 | 48 | - `enter`: It will create a new bullet point 49 | - `tab` : it will move the bullet to the right 50 | - `shift+tag`: it will move the bullet to the left 51 | 52 | - `ctrl+alt+c` : add the $complete tag to the current tag and removes the #task and $todo tags 53 | - `ctrl+alt+s` : add #current_sprint and #sprint2 to the current note, when current sprint is 2 54 | - `ctrl+alt+f` : focus on the filter box 55 | - `alt+click (on bullet point)`: collapse/expand notes 56 | 57 | - `ctrl+alt+x` : cut note 58 | - `ctrl+alt+v` : paste note 59 | 60 | 61 | ##### Sprints and tasks 62 | You can organize your tasks by sprint, adding the #sprint1 hashtag and #current_sprint. For example, you have 3 tasks to complete this week and 2 to complete next week. So for the first 3 you add the hashtag #sprint1 and #current_sprint and for the last 2 you add just the hash_tag #sprint2. Then the next week, you can use the "change sprint" functionality, write 2 and click "change sprint" on the upper left sidebar. This will add #current_sprint to the #sprint2 hashtags. 63 | 64 | ## Installation 65 | 66 | It uses my library [gDriveSync.js](https://github.com/vitogit/gDriveSync.js) for syncing with google drive, so first you will need to: 67 | Add your api client_id to config.js. You can get the client id following the instruction from step1 here https://developers.google.com/drive/v3/web/quickstart/js 68 | 69 | Then just install the app: 70 | 71 | ``` 72 | npm install 73 | npm start 74 | ``` 75 | 76 | go to http://localhost:4000 77 | 78 | To run tests go to 79 | http://localhost:4000/test/ 80 | -------------------------------------------------------------------------------- /public/test/sorter-spec.js: -------------------------------------------------------------------------------- 1 | var expect = chai.expect 2 | 3 | describe('Sorter', function() { 4 | before(function() { 5 | var editor = $('
'); 6 | var allTags = $('
') 7 | var sprints = $('
') 8 | $('body').append(editor).append(allTags).append(sprints) 9 | sorter = new Sorter(editor) 10 | }); 11 | 12 | it('exists', function() { 13 | expect(sorter).to.exist 14 | }) 15 | 16 | it('has a editor property ', function() { 17 | expect(sorter.editor).to.exist 18 | }) 19 | 20 | it('filter by hash', function() { 21 | expect(visibleRows()).to.be.eq(5) 22 | sorter.filter('#hash1'); 23 | expect(visibleRows()).to.be.eq(2) //root and li 24 | }) 25 | 26 | it('filter by smarttag', function() { 27 | sorter.filter('$completed'); 28 | expect(visibleRows()).to.be.eq(3) //root and child and grandchild 29 | }) 30 | 31 | it('filter by 2 hashtags using OR ', function() { 32 | sorter.filter('#hash1 #hash2'); 33 | expect(visibleRows()).to.be.eq(5) //root and everyone 34 | }) 35 | 36 | it('filter by 2 hashtags using AND ', function() { 37 | sorter.filter('& #hash1 #sprint1', [], 'AND'); 38 | expect(visibleRows()).to.be.eq(2) //root and the first 39 | 40 | sorter.filter('& #sprint1 #hash1', [], 'AND'); 41 | expect(visibleRows()).to.be.eq(2) //root and the first 42 | }) 43 | 44 | it('hide completed and search hashtags', function() { 45 | sorter.filter('#hash2', ['$completed']); 46 | expect(visibleRows()).to.be.eq(3) //root, child2 and 1 grandchild (hide one) 47 | }) 48 | 49 | it('parse the hashtags', function() { 50 | $(sorter.editor).find('li').show() 51 | sorter.parseHashtags(); 52 | var parsedCount = $('.hash_link').length 53 | expect(parsedCount).to.be.eq(4) 54 | }) 55 | 56 | it('parse the smartTags', function() { 57 | $(sorter.editor).find('li').show() 58 | sorter.parseSmartTags(); 59 | var parsedCount = $('.smartTag').length 60 | expect(parsedCount).to.be.eq(1) 61 | }) 62 | 63 | it('parse the notebooks links', function() { 64 | $(sorter.editor).find('li').show() 65 | sorter.parseNotebookTags(); 66 | var parsedCount = $('.internal_notebook_link').length 67 | expect(parsedCount).to.be.eq(2) 68 | }) 69 | 70 | it('extract all the tags', function() { 71 | sorter.extractTags('smartTag','$'); 72 | sorter.extractTags('hash_link','#'); 73 | var tagsCount = $('#allTags a').length 74 | expect(tagsCount).to.be.eq(5) 75 | }) 76 | 77 | it('extract sprint tags', function() { 78 | sorter.extractSprintTags(); 79 | var tagsCount = $('#sprints a').length 80 | expect(tagsCount).to.be.eq(2) 81 | }) 82 | 83 | it('extract current sprint tags', function() { 84 | $('#editor').remove() 85 | $('#sprints').remove() 86 | var editor = $('
'); 87 | var sprints = $('
') 88 | $('body').append(editor).append(sprints) 89 | sorter = new Sorter(editor) 90 | 91 | sorter.extractCurrentSprintTags(); 92 | var currentSprint = $('#sprints').text() 93 | expect(currentSprint).to.be.eq('#current_sprint (3)$todo(1)#task(1)$completed(1)') 94 | expect(countHashtag('#current_sprint')).to.be.eq(3) 95 | }) 96 | 97 | it('changes the sprint', function() { 98 | $('#editor').remove() 99 | $('#sprints').remove() 100 | var editor = $('
'); 101 | var sprints = $('
') 102 | $('body').append(editor).append(sprints) 103 | sorter = new Sorter(editor) 104 | 105 | expect(countHashtag('#sprint1')).to.be.eq(3) 106 | expect(countHashtag('#sprint2')).to.be.eq(1) 107 | sorter.moveToSprint(2); //change the current sprint to 2 from 1 108 | expect(countHashtag('#sprint1')).to.be.eq(3) 109 | expect(countHashtag('#sprint2')).to.be.eq(1) 110 | expect(countHashtag('#current_sprint')).to.be.eq(1) 111 | }) 112 | 113 | it('get current_sprint number', function() { 114 | var sprintNumber = sorter.getSprintNumber(); 115 | expect(sprintNumber).to.be.eq('2') 116 | }) 117 | 118 | xit('get hashtags and parents in text [currently not used]', function() { 119 | $('#editor').remove() 120 | var editor = $('
'); 121 | $('body').append(editor) 122 | sorter = new Sorter(editor) 123 | 124 | var tags = sorter.getTagAndParents('#task') 125 | 126 | expect(tags.length).to.be.eq(2) 127 | expect(tags[0]).to.be.eq(' root | child1 #task') 128 | expect(tags[1]).to.be.eq(' root | child2 #hash2 | grandchild1 #task') 129 | }) 130 | 131 | 132 | function visibleRows() { 133 | return $(sorter.editor).find('li').filter(function() { 134 | return $(this).css('display') !== 'none'; 135 | }).length; 136 | } 137 | 138 | function countHashtag(hashtag) { 139 | return $(sorter.editor).find('li').filter(function() { 140 | var li_text = $(this).clone().children('ul').remove().end().html(); 141 | return li_text.includes(hashtag); 142 | }).length; 143 | } 144 | 145 | }) 146 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Sorter: Ideas and task organizer 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 |
21 |

Please sign-in with your Google account to continue.

22 | 23 |
24 | 25 |
26 |
27 |

Sorter

- Sign-Out 28 |
29 |
30 | 76 |
77 | 80 |
81 |
82 | 83 |
84 |
85 |
    86 | 87 |
88 |
    89 | 90 |
91 |
92 |
journal
93 |
94 |
95 |
96 |
97 |
98 |

FileFinder

99 |
100 |
101 |
    102 |
    103 |
    104 |
    105 | 106 | 193 | 194 | 195 | 196 | -------------------------------------------------------------------------------- /public/sorter.js: -------------------------------------------------------------------------------- 1 | var Sorter = function(editorId) { 2 | this.editor = editorId; 3 | 4 | //current_text: space separated string, by which will filter the elements 5 | //tagsToHide: array of tags to hide 6 | //type_and: filter using and instead of or 7 | this.filter = function(current_text, tagsToHide, type_and) { 8 | 9 | current_text = current_text || '' 10 | tagsToHide = tagsToHide || [] 11 | var self = this 12 | var hashtags = current_text.replace(/&/g, '').replace(/ +/g, ' ').replace(/\$/g, '\\$').split(' ') 13 | hashtags = hashtags.filter(function(h){ return h != "" }); 14 | var tagsToHide = tagsToHide.map(function(e) { 15 | return e.replace(/\$/g, '\\$'); 16 | }) 17 | tagsToHide = tagsToHide.filter(function(h){ return h != "" }); 18 | 19 | //filter using OR 20 | var regex = hashtags.join("|") 21 | if (type_and) { //filter using AND 22 | regex = hashtags.map(function(e) { 23 | return '(?=.*'+e+')' 24 | }).join("") 25 | } 26 | $(this.editor).find('.filtered').removeClass('filtered') 27 | $(this.editor).find('li').hide() 28 | $(this.editor).find('li').each(function() { 29 | var li_text = $(this).clone().children('ul').remove().end().html(); 30 | if (new RegExp(regex,'i').test(li_text)) { 31 | if (current_text !='') { 32 | $(this).addClass('filtered'); 33 | } 34 | $(this).show() 35 | $(this).parents().show() 36 | $(this).find('li').show() 37 | } 38 | 39 | if (tagsToHide.length && new RegExp(tagsToHide.join("|"), 'i').test(li_text)) { 40 | $(this).hide() 41 | $(this).find('li').hide() 42 | } 43 | }) 44 | if (current_text == '') { 45 | $(this.editor).find('.closed-icon').children('ul').hide() 46 | } 47 | } 48 | 49 | this.parseHashtags = function() { 50 | var initText = $(this.editor).html() 51 | var parsedText = initText.replace( /#(\w+)\b(?!<\/a>)/g ,'#$1') 52 | $(this.editor).html(parsedText); 53 | } 54 | 55 | this.parseSmartTags = function() { 56 | var initText = $(this.editor).html() 57 | 58 | var parsedText = initText.replace(/\$(\w+)\b(?!<\/a>)/g, function (match, smartTag) { 59 | var newLink = $("", { 60 | href : "#", 61 | class : 'smartTag', 62 | 'data-name': smartTag, 63 | 'data-created-at': new Date().getTime(), 64 | text : '$'+smartTag 65 | }) 66 | 67 | if (smartTag == 'completed') { 68 | newLink.addClass('completed bg-success') 69 | } else if (smartTag == 'todo') { 70 | newLink.addClass('todo bg-warning') 71 | } else if (smartTag == 'journal') { 72 | newLink.addClass('journal bg-info') 73 | } else { 74 | 75 | } 76 | return newLink.prop('outerHTML'); 77 | }); 78 | 79 | $(this.editor).html(parsedText); 80 | } 81 | 82 | this.parseNotebookTags = function() { 83 | var initText = $(this.editor).html() 84 | 85 | var parsedText = initText.replace(/\ @(\w+)\b(?!<\/a>)/g, function (match, notebook) { 86 | var dataId = $('#notebooks .notebook_link:contains("'+notebook+'")').data('id'); 87 | var newLink = $("", { 88 | href : "#", 89 | class : 'internal_notebook_link', 90 | 'data-name': notebook, 91 | 'data-id': dataId, 92 | text : '@'+notebook 93 | }) 94 | return newLink.prop('outerHTML'); 95 | }); 96 | $(this.editor).html(parsedText); 97 | } 98 | 99 | this.extractTags = function(class_name, type) { 100 | var tagMap = {} 101 | $(this.editor).find('a.'+class_name).each(function(){ 102 | var name = $(this).data('name') 103 | if (tagMap[name]) { 104 | tagMap[name] = tagMap[name]+1 105 | } else { 106 | tagMap[name] = 1 107 | } 108 | }) 109 | 110 | var tagMapSorted = {}; 111 | Object.keys(tagMap).sort().forEach(function(key) { 112 | tagMapSorted[key] = tagMap[key]; 113 | }); 114 | 115 | $.each(tagMapSorted, function( name, count ) { 116 | var newLink = $("", { 117 | 'data-name': name, 118 | href : "#", 119 | text : type+name+"("+count+")", 120 | class: 'filter_link' 121 | }); 122 | 123 | $('#allTags').append(newLink).append('
    ') 124 | }); 125 | } 126 | 127 | this.extractCurrentSprintTags = function() { 128 | var tagMap = {'current_sprint':0, '$todo':0, '#task':0, '$completed':0,} 129 | 130 | $(this.editor).find('li').each(function(){ 131 | var li_text = $(this).clone().children('ul').remove().end().text(); 132 | 133 | if (li_text.includes('#current_sprint')) { 134 | tagMap['current_sprint']++ 135 | if (li_text.includes('$todo')) { 136 | tagMap['$todo']++ 137 | } 138 | if (li_text.includes('#task')) { 139 | tagMap['#task']++ 140 | } 141 | if (li_text.includes('$completed')) { 142 | tagMap['$completed']++ 143 | } 144 | } 145 | }) 146 | var currentSprint = $("
    ", { 147 | 'data-name': 'current_sprint', 148 | href : "#", 149 | text : "#current_sprint ("+tagMap['current_sprint']+")", 150 | class: 'bookmark_link' 151 | }); 152 | 153 | $('#sprints').append(currentSprint).append('
    ') 154 | delete tagMap['current_sprint'] 155 | 156 | $.each(tagMap, function( name, count ) { 157 | var type = name[0]; 158 | var newLink = $("
    ", { 159 | 'data-name': name.substring(1, name.length), 160 | href : "#", 161 | text : name+"("+count+")", 162 | class: 'sub_bookmark_link' 163 | }); 164 | 165 | $('#sprints').append(newLink).append('
    ') 166 | }); 167 | $('#sprints').append('
    ') 168 | } 169 | 170 | this.extractSprintTags = function() { 171 | var tagMap = {} 172 | $(this.editor).find('a.hash_link:contains("sprint")').each(function(){ 173 | var name = $(this).data('name') 174 | if (name.startsWith('sprint')) { 175 | if (tagMap[name]) { 176 | tagMap[name] = tagMap[name]+1 177 | } else { 178 | tagMap[name] = 1 179 | } 180 | } 181 | }) 182 | 183 | var tagMapSorted = {}; 184 | Object.keys(tagMap).sort().forEach(function(key) { 185 | tagMapSorted[key] = tagMap[key]; 186 | }); 187 | 188 | $.each(tagMapSorted, function( name, count ) { 189 | var number = name[name.length-1]; 190 | var singleName = name.substring(0, name.length - 1); 191 | var newLink = $("
    ", { 192 | 'data-name': name, 193 | href : "#", 194 | text : "#"+singleName+" "+number+" ("+count+")", 195 | class: 'bookmark_link' 196 | }); 197 | 198 | $('#sprints').append(newLink).append('
    ') 199 | }); 200 | } 201 | 202 | this.moveToSprint = function(sprintNumber) { 203 | $(this.editor).find('a.hash_link').each(function(){ 204 | var name = $(this).data('name') 205 | if (name =='current_sprint') { //remove old current_sprint 206 | $(this).remove(); 207 | } 208 | if (name == 'sprint'+sprintNumber) { //append current sprint 209 | $(this).after('
    #current_sprint') 210 | } 211 | }) 212 | } 213 | 214 | this.getSprintNumber = function() { 215 | var sprintNumber; 216 | $(this.editor).find('li').each(function() { 217 | var li_text = $(this).clone().children('ul').remove().end().html(); 218 | if (li_text.includes('#current_sprint')) { 219 | sprintNumber = (/#sprint(\d+)/g).exec(li_text)[1]; 220 | return false; //break loop 221 | } 222 | }) 223 | return sprintNumber; 224 | } 225 | 226 | this.removeJunk = function() { 227 | $(this.editor).find('ul, li').each(function(){ 228 | if (!$(this).attr('class')) { 229 | $(this).removeAttr('class'); 230 | } 231 | if (!$(this).attr('style')) { 232 | $(this).removeAttr('style'); 233 | } 234 | }) 235 | $(this.editor).find('[data-mce-href]').removeAttr('data-mce-href') 236 | $(this.editor).find('[data-mce-style]').removeAttr('data-mce-style') 237 | } 238 | 239 | this.getTagAndParents = function(hashtags) { 240 | var self = this 241 | var hashtags = hashtags.replace(/ +/g, ' ').replace(/\$/g, '\\$').split(' ') 242 | hashtags = hashtags.filter(function(h){ return h != "" }); 243 | var fullTags = []; 244 | $(this.editor).find('li').each(function() { 245 | var li_text = $(this).clone().children('ul').remove().end().text(); 246 | //filter using OR 247 | if (new RegExp(hashtags.join("|"), 'i').test(li_text)) { 248 | var fullTag = li_text; 249 | var lis = $(this).parents('li').each(function() { 250 | var parentText = $(this).clone().children('ul').remove().end().text(); 251 | fullTag = parentText +' | '+ fullTag 252 | }) 253 | // fullTag = fullTag + li_text 254 | fullTag = fullTag.replace(/\s+ /g, ' '); 255 | fullTags.push(fullTag); 256 | } 257 | }) 258 | return fullTags; 259 | } 260 | 261 | } 262 | -------------------------------------------------------------------------------- /public/main.js: -------------------------------------------------------------------------------- 1 | var App = function() { 2 | 3 | var self = this 4 | 5 | this.sorter; 6 | 7 | this.tinyDom; 8 | this.tempNote; 9 | 10 | this.init = function() { 11 | 12 | $('#container').on('click', '.bookmark_link', function(){ 13 | app.filterBox(this) 14 | }) 15 | 16 | $('#container').on('click', '.sub_bookmark_link', function(){ 17 | var type = $(this).text()[0]; 18 | var hashtag = type+$(this).data('name') 19 | $('#filter_box').val('& #current_sprint '+hashtag) 20 | $('#filter_box').trigger("input") 21 | }) 22 | 23 | $('#container').on('click', '.notebook_link', function(){ 24 | var fileId = $(this).data('id'); 25 | app.loadNotes(fileId); 26 | }) 27 | 28 | $('#container').on('click', '.filter_clear', function(){ 29 | $('#filter_box').val("") 30 | app.filter() 31 | }) 32 | 33 | $('#container').on('click', '.filter_link', function(){ 34 | app.filterBox(this) 35 | }) 36 | 37 | $('#container').on('click', '.hideThisTag', function(){ 38 | app.filter(); 39 | }) 40 | 41 | $('.router').click(function(){ 42 | var name = $(this).data('name'); 43 | $('.router').removeClass(); 44 | $(this).addClass('active'); 45 | 46 | $('.visual').hide(); 47 | $("#"+name).show(); 48 | }) 49 | 50 | $('.router').click(function(){ 51 | var name = $(this).data('name'); 52 | if (name == "tasks") { 53 | app.loadTaskView(); 54 | } 55 | 56 | $('.router').removeClass(); 57 | $(this).addClass('active'); 58 | 59 | $('.visual').hide(); 60 | $("#"+name).show(); 61 | }) 62 | 63 | tinymce.init({ 64 | selector: '#editor', 65 | forced_root_block : '', 66 | width: '100%', 67 | height: '100%', 68 | statusbar: false, 69 | menubar:false, 70 | keep_styles: false, 71 | content_css : './assets/css/simplex.bootstrap.min.css, ./assets/css/editor.css', 72 | plugins: [ 73 | 'autolink lists link save autoresize codesample' 74 | ], 75 | save_onsavecallback: function () { app.saveCallback() }, 76 | toolbar: 'bullist save removeformat codesample', 77 | codesample_languages: [ 78 | {text: 'JavaScript', value: 'javascript'}, 79 | {text: 'Ruby', value: 'ruby'}, 80 | {text: 'HTML/XML', value: 'markup'}, 81 | ], 82 | setup : function(ed){ 83 | ed.on('keyup',function(e){ 84 | if ( 13 === e.keyCode ) { //after enter 85 | var currentElement = $(ed.selection.getNode()).closest('li') 86 | if ($(currentElement).hasClass('closed-icon')) { 87 | currentElement.prev().append(currentElement.children('ul')) 88 | } 89 | currentElement.removeAttr('class'); //remove previous class 90 | } 91 | }); 92 | ed.on('SaveContent', function() { 93 | app.sorter.removeJunk(); 94 | app.saveBookmark(); 95 | app.parseText(); 96 | app.applyStyles(); 97 | }), 98 | ed.on('init', function() { 99 | this.getDoc().body.style.fontSize = '14px'; 100 | app.tinyDom = tinyMCE.activeEditor.dom.getRoot() 101 | app.sorter = new Sorter(app.tinyDom); 102 | $(app.tinyDom).on('click', 'li', function(e){ 103 | e.stopPropagation(); 104 | if (e.altKey) { 105 | app.toggleNote(this) 106 | } 107 | }) 108 | 109 | $(app.tinyDom).on('click', '.hash_link, a.smartTag', function(){ 110 | app.filterBox(this) 111 | }) 112 | $(app.tinyDom).on('click', '.internal_notebook_link', function(){ 113 | var confirm1 = confirm('Are you sure? This will open a document and unsaved changes will be lost'); 114 | if (confirm1) { 115 | var fileId = $(this).data('id'); 116 | app.loadNotes(fileId); 117 | } 118 | }) 119 | $(app.tinyDom).on('keydown', function(e){ 120 | var cKey = 67 == e.keyCode; 121 | var sKey = 83 == e.keyCode; 122 | var fKey = 70 == e.keyCode; 123 | var xKey = 88 == e.keyCode; 124 | var vKey = 86 == e.keyCode; 125 | var node = ed.selection.getNode() 126 | var sprintNumber = app.sorter.getSprintNumber(); 127 | if (e.altKey && e.ctrlKey && xKey ) { //cut note 128 | if ($(node).is('li')) { 129 | app.tempNote = $(node); 130 | } else { 131 | app.tempNote = $(node).parent('li'); 132 | } 133 | } 134 | if (e.altKey && e.ctrlKey && vKey ) { //paste note 135 | if ($(node).children('ul').length == 0 ) { 136 | $(node).append('