├── .editorconfig ├── .eslintrc.json ├── .gitattributes ├── .gitignore ├── LICENSE ├── README.md ├── app ├── _locales │ └── en │ │ └── messages.json ├── images │ ├── calendar.gif │ ├── favicon.ico │ ├── focus_mindmap.gif │ ├── focus_text.gif │ ├── icon-128.png │ ├── icon-144.png │ ├── icon-16.png │ ├── mindmap.gif │ └── text.gif ├── index.html ├── libs │ └── ace │ │ └── ace.js ├── manifest.json ├── manifest_for_pwa.json ├── scripts │ ├── background.js │ ├── bounds.js │ ├── chrome_work_storage.js │ ├── firebase_work_storage.js │ ├── local_work_storage.js │ ├── mindmap.js │ ├── newtab.js │ ├── node.js │ ├── parser.js │ ├── service_worker.js │ ├── service_worker_loader.js │ ├── token.js │ └── work.js └── styles │ └── newtab.css ├── gulpfile.js ├── package.json ├── test ├── index.html └── spec │ └── test.js ├── webpack.config.js └── yarn.lock /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig helps developers define and maintain consistent 2 | # coding styles between different editors and IDEs 3 | # editorconfig.org 4 | 5 | root = true 6 | 7 | 8 | [*] 9 | 10 | # Change these settings to your own preference 11 | indent_style = space 12 | indent_size = 2 13 | 14 | [*.json] 15 | indent_size = 2 16 | 17 | # We recommend you to keep these unchanged 18 | end_of_line = lf 19 | charset = utf-8 20 | trim_trailing_whitespace = true 21 | insert_final_newline = true 22 | 23 | [*.md] 24 | trim_trailing_whitespace = false 25 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["eslint:recommended"], 3 | "plugins": [], 4 | "parserOptions": { 5 | "sourceType": "module" 6 | }, 7 | "env": { 8 | "browser": true, 9 | "es6": true 10 | }, 11 | "globals": { 12 | "chrome": false, 13 | "$": false, 14 | "firebase": false, 15 | "ace": false, 16 | "ResponsiveBootstrapToolkit": false, 17 | "require": false 18 | }, 19 | "rules": { 20 | "no-console": "off", 21 | "semi": "error", 22 | "no-var": "error", 23 | "no-unused-vars": ["error", {"argsIgnorePattern": "^_"}], 24 | "quotes": ["error", "single"] 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | temp 3 | .tmp 4 | dist 5 | .sass-cache 6 | package 7 | app/service_worker*.js 8 | yarn-error.log 9 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | MindMap Tab 2 | =========== 3 | 4 | This is a Chrome extension to draw Mind Map diagram quickly and easily on New Tab for Chrome. 5 | 6 | License 7 | ------- 8 | 9 | Apache License Version 2.0 10 | 11 | -------------------------------------------------------------------------------- /app/_locales/en/messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "appName": { 3 | "message": "MindMap Tab", 4 | "description": "The name of the application" 5 | }, 6 | "appDescription": { 7 | "message": "Keep your idea by drawing MindMap quickly.", 8 | "description": "The description of the application" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /app/images/calendar.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yoichiro/mindmap_tab/ca4463632a93c11405edefe215ffeea2c09e1811/app/images/calendar.gif -------------------------------------------------------------------------------- /app/images/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yoichiro/mindmap_tab/ca4463632a93c11405edefe215ffeea2c09e1811/app/images/favicon.ico -------------------------------------------------------------------------------- /app/images/focus_mindmap.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yoichiro/mindmap_tab/ca4463632a93c11405edefe215ffeea2c09e1811/app/images/focus_mindmap.gif -------------------------------------------------------------------------------- /app/images/focus_text.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yoichiro/mindmap_tab/ca4463632a93c11405edefe215ffeea2c09e1811/app/images/focus_text.gif -------------------------------------------------------------------------------- /app/images/icon-128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yoichiro/mindmap_tab/ca4463632a93c11405edefe215ffeea2c09e1811/app/images/icon-128.png -------------------------------------------------------------------------------- /app/images/icon-144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yoichiro/mindmap_tab/ca4463632a93c11405edefe215ffeea2c09e1811/app/images/icon-144.png -------------------------------------------------------------------------------- /app/images/icon-16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yoichiro/mindmap_tab/ca4463632a93c11405edefe215ffeea2c09e1811/app/images/icon-16.png -------------------------------------------------------------------------------- /app/images/mindmap.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yoichiro/mindmap_tab/ca4463632a93c11405edefe215ffeea2c09e1811/app/images/mindmap.gif -------------------------------------------------------------------------------- /app/images/text.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yoichiro/mindmap_tab/ca4463632a93c11405edefe215ffeea2c09e1811/app/images/text.gif -------------------------------------------------------------------------------- /app/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | MindMap Tab 24 | 25 | 26 | 27 | 178 | 179 |
180 |
181 |
182 |
183 |
184 |
185 |
186 | 187 |
188 |
189 |
190 |
191 | 192 | 204 | 205 | 224 | 225 | 240 | 241 | 272 | 273 | 306 | 307 | 308 | 309 | 310 | 311 | 312 | 313 | -------------------------------------------------------------------------------- /app/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "__MSG_appName__", 3 | "version": "3.3.2", 4 | "manifest_version": 2, 5 | "description": "__MSG_appDescription__", 6 | "icons": { 7 | "16": "images/icon-16.png", 8 | "128": "images/icon-128.png" 9 | }, 10 | "default_locale": "en", 11 | "permissions": [ 12 | "storage", 13 | "unlimitedStorage", 14 | "topSites", 15 | "https://www.googleapis.com/" 16 | ], 17 | "content_security_policy": "script-src 'self' https://www.gstatic.com/ https://*.firebaseio.com https://www.googleapis.com; object-src 'self'", 18 | "browser_action": { 19 | "default_icon": { 20 | "16": "images/icon-16.png" 21 | } 22 | }, 23 | "background": { 24 | "scripts": ["scripts/background.js"], 25 | "persistent": false 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /app/manifest_for_pwa.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "MindMap Tab", 3 | "name": "MindMap Tab", 4 | "icons": [ 5 | { 6 | "src": "images/icon-128.png", 7 | "type": "image/png", 8 | "sizes": "128x128" 9 | }, 10 | { 11 | "src": "images/icon-144.png", 12 | "type": "image/png", 13 | "sizes": "144x144" 14 | } 15 | ], 16 | "start_url": "index.html", 17 | "display": "standalone" 18 | } 19 | -------------------------------------------------------------------------------- /app/scripts/background.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | chrome.browserAction.onClicked.addListener(() => { 4 | chrome.tabs.create({ 5 | url: chrome.runtime.getURL('index.html') 6 | }); 7 | }); -------------------------------------------------------------------------------- /app/scripts/bounds.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | export default class Bounds { 4 | 5 | constructor(x, y, width, height) { 6 | this.x = x; 7 | this.x1 = x; 8 | this.y = y; 9 | this.y1 = y; 10 | this.width = width; 11 | this.height = height; 12 | this.x2 = x + width; 13 | this.y2 = y + height; 14 | } 15 | 16 | } 17 | -------------------------------------------------------------------------------- /app/scripts/chrome_work_storage.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import Work from './work.js'; 4 | 5 | export default class ChromeWorkStorage { 6 | 7 | constructor(newtab) { 8 | this._newtab = newtab; 9 | } 10 | 11 | // Public functions 12 | 13 | initialize(callback) { 14 | if (callback) { 15 | callback(); 16 | } 17 | } 18 | 19 | canProvideTopSites() { 20 | return true; 21 | } 22 | 23 | logout(callback) { 24 | if (callback) { 25 | callback(); 26 | } 27 | } 28 | 29 | save(work, callback) { 30 | if (work.isSave && work.hasContent()) { 31 | this._getAll(contentMap => { 32 | // TODO: Should check exists and updated 33 | work.updated = Date.now(); 34 | contentMap[work.created] = { 35 | created: work.created, 36 | content: work.content, 37 | updated: work.updated 38 | }; 39 | chrome.storage.local.set( 40 | { 41 | contentMap: contentMap 42 | }, () => { 43 | if (callback) { 44 | callback(); 45 | } 46 | } 47 | ); 48 | }); 49 | } else { 50 | if (callback) { 51 | callback(); 52 | } 53 | } 54 | } 55 | 56 | getAll(callback) { 57 | this._getAll(contentMap => { 58 | this.getAllKeys(keys => { 59 | callback(keys.map(key => { 60 | let data = contentMap[key]; 61 | return new Work(data.created, data.content, data.updated); 62 | })); 63 | }); 64 | }); 65 | } 66 | 67 | getAllKeys(callback) { 68 | this._getAll(contentMap => { 69 | let keys = Object.keys(contentMap); 70 | callback(keys.sort((a, b) => { 71 | return b - a; 72 | })); 73 | }); 74 | } 75 | 76 | getLast(callback) { 77 | this._getAll(contentMap => { 78 | let keys = Object.keys(contentMap); 79 | if (keys && keys.length > 0) { 80 | let max = keys.reduce((p, c) => { 81 | return contentMap[p].updated > contentMap[c].updated ? p : c; 82 | }); 83 | let data = contentMap[max]; 84 | callback(new Work(data.created, data.content, data.updated)); 85 | } else { 86 | callback(null); 87 | } 88 | }); 89 | } 90 | 91 | removeAll(callback) { 92 | chrome.storage.local.set({ 93 | contentMap: {} 94 | }, () => { 95 | callback(); 96 | }); 97 | } 98 | 99 | remove(work, callback) { 100 | this._getAll(contentMap => { 101 | delete contentMap[work.created]; 102 | chrome.storage.local.set({ 103 | contentMap: contentMap 104 | }, () => { 105 | callback(); 106 | }); 107 | }); 108 | } 109 | 110 | // Private functions 111 | 112 | _getAll(callback) { 113 | chrome.storage.local.get('contentMap', item => { 114 | callback(item.contentMap || {}); 115 | }); 116 | } 117 | 118 | } -------------------------------------------------------------------------------- /app/scripts/firebase_work_storage.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import Work from './work.js'; 4 | 5 | export default class FirebaseWorkStorage { 6 | 7 | constructor(newtab) { 8 | this._newtab = newtab; 9 | this._initializeFirebase(); 10 | } 11 | 12 | // Public functions 13 | 14 | initialize(callback) { 15 | let unsubscribe = firebase.auth().onAuthStateChanged(user => { 16 | unsubscribe(); 17 | if (user) { 18 | this._startObservation(); 19 | callback(true); 20 | } else { 21 | callback(false); 22 | } 23 | }); 24 | } 25 | 26 | canProvideTopSites() { 27 | return false; 28 | } 29 | 30 | logout(callback) { 31 | this._stopObservation(); 32 | firebase.auth().signOut() 33 | .then(() => { 34 | if (callback) { 35 | callback(); 36 | } 37 | }) 38 | .catch(error => { 39 | console.error(error); 40 | if (callback) { 41 | callback(); 42 | } 43 | }); 44 | } 45 | 46 | createUser(email, password, successCallback, failureCallback) { 47 | firebase.auth().createUserWithEmailAndPassword(email, password) 48 | .then(() => { 49 | this._startObservation(); 50 | if (successCallback) { 51 | successCallback(); 52 | } 53 | }) 54 | .catch(error => { 55 | if (failureCallback) { 56 | failureCallback(error); 57 | } 58 | }); 59 | } 60 | 61 | sendPasswordResetEmail(email, successCallback, failureCallback) { 62 | firebase.auth().sendPasswordResetEmail(email) 63 | .then(() => { 64 | if (successCallback) { 65 | successCallback(); 66 | } 67 | }) 68 | .catch(error => { 69 | if (failureCallback) { 70 | failureCallback(error); 71 | } 72 | }); 73 | } 74 | 75 | login(email, password, successCallback, failureCallback) { 76 | firebase.auth().signInWithEmailAndPassword(email, password) 77 | .then(() => { 78 | this._startObservation(); 79 | if (successCallback) { 80 | successCallback(); 81 | } 82 | }) 83 | .catch((error) => { 84 | if (failureCallback) { 85 | failureCallback(error); 86 | } 87 | }); 88 | } 89 | 90 | getCurrentUserEmail() { 91 | const user = firebase.auth().currentUser; 92 | if (user) { 93 | return user.email; 94 | } else { 95 | return ''; 96 | } 97 | } 98 | 99 | save(work, callback) { 100 | if (work.isSave && work.hasContent()) { 101 | const myRootRef = this._getMyRootRef(); 102 | myRootRef.once('value').then(snapshot => { 103 | let contentMap = snapshot.val() || {}; 104 | // TODO: Should check exists and updated 105 | work.updated = Date.now(); 106 | contentMap[work.created] = { 107 | created: work.created, 108 | content: work.content, 109 | updated: work.updated 110 | }; 111 | myRootRef.set(contentMap) 112 | .then(() => { 113 | if (callback) { 114 | callback(); 115 | } 116 | }) 117 | .catch(error => { 118 | console.error(error); 119 | if (callback) { 120 | callback(); 121 | } 122 | }); 123 | }); 124 | } else { 125 | if (callback) { 126 | callback(); 127 | } 128 | } 129 | } 130 | 131 | getAll(callback) { 132 | this._getAll(contentMap => { 133 | this.getAllKeys(keys => { 134 | callback(keys.map(key => { 135 | let data = contentMap[key]; 136 | return new Work(data.created, data.content, data.updated); 137 | })); 138 | }); 139 | }); 140 | } 141 | 142 | getAllKeys(callback) { 143 | this._getAll(contentMap => { 144 | let keys = Object.keys(contentMap); 145 | callback(keys.sort((a, b) => { 146 | return b - a; 147 | })); 148 | }); 149 | } 150 | 151 | getLast(callback) { 152 | this._getAll(contentMap => { 153 | let keys = Object.keys(contentMap); 154 | if (keys && keys.length > 0) { 155 | let max = keys.reduce((p, c) => { 156 | return contentMap[p].updated > contentMap[c].updated ? p : c; 157 | }); 158 | let data = contentMap[max]; 159 | callback(new Work(data.created, data.content, data.updated)); 160 | } else { 161 | callback(null); 162 | } 163 | }); 164 | } 165 | 166 | removeAll(callback) { 167 | callback(); 168 | } 169 | 170 | remove(work, callback) { 171 | this._getAll(contentMap => { 172 | delete contentMap[work.created]; 173 | this._getMyRootRef().set(contentMap) 174 | .then(() => { 175 | if (callback) { 176 | callback(); 177 | } 178 | }); 179 | }); 180 | } 181 | 182 | // Private functions 183 | 184 | _getAll(callback) { 185 | this._getMyRootRef().once('value') 186 | .then(snapshot => { 187 | let contentMap = snapshot.val() || {}; 188 | callback(contentMap); 189 | }); 190 | } 191 | 192 | _getMyRootRef() { 193 | let user = firebase.auth().currentUser; 194 | let database = firebase.database(); 195 | let myRootRef = database.ref('mindmaps/private/' + user.uid); 196 | return myRootRef; 197 | } 198 | 199 | _initializeFirebase() { 200 | const config = { 201 | apiKey: 'AIzaSyCZDGsxx5VbFDwo9lRe2vDuWf4aS5-XmNc', 202 | databaseURL: 'https://mindmap-tab.firebaseio.com' 203 | }; 204 | firebase.initializeApp(config); 205 | } 206 | 207 | _startObservation() { 208 | this._getMyRootRef().on('child_added', snapshot => { 209 | this._newtab.onWorkAdded(snapshot.key, this._createWork(snapshot)); 210 | }); 211 | this._getMyRootRef().on('child_changed', snapshot => { 212 | this._newtab.onWorkChanged(snapshot.key, this._createWork(snapshot)); 213 | }); 214 | this._getMyRootRef().on('child_removed', snapshot => { 215 | this._newtab.onWorkRemoved(snapshot.key, this._createWork(snapshot)); 216 | }); 217 | } 218 | 219 | _createWork(snapshot) { 220 | const data = snapshot.val(); 221 | return new Work(data.created, data.content, data.updated); 222 | } 223 | 224 | _stopObservation() { 225 | this._getMyRootRef().off(); 226 | } 227 | 228 | } 229 | -------------------------------------------------------------------------------- /app/scripts/local_work_storage.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import Work from './work.js'; 4 | 5 | export default class LocalWorkStorage { 6 | 7 | constructor(newtab) { 8 | this._newtab = newtab; 9 | } 10 | 11 | // Public functions 12 | 13 | initialize(callback) { 14 | if (callback) { 15 | callback(); 16 | } 17 | } 18 | 19 | canProvideTopSites() { 20 | return false; 21 | } 22 | 23 | logout(callback) { 24 | if (callback) { 25 | callback(); 26 | } 27 | } 28 | 29 | save(work, callback) { 30 | if (work.isSave && work.hasContent()) { 31 | this._getAll(contentMap => { 32 | // TODO: Should check exists and updated 33 | work.updated = Date.now(); 34 | contentMap[work.created] = { 35 | created: work.created, 36 | content: work.content, 37 | updated: work.updated 38 | }; 39 | localStorage.setItem('contentMap', JSON.stringify(contentMap)); 40 | if (callback) { 41 | callback(); 42 | } 43 | }); 44 | } else { 45 | if (callback) { 46 | callback(); 47 | } 48 | } 49 | } 50 | 51 | getAll(callback) { 52 | this._getAll(contentMap => { 53 | this.getAllKeys(keys => { 54 | callback(keys.map(key => { 55 | let data = contentMap[key]; 56 | return new Work(data.created, data.content, data.updated); 57 | })); 58 | }); 59 | }); 60 | } 61 | 62 | getAllKeys(callback) { 63 | this._getAll(contentMap => { 64 | let keys = Object.keys(contentMap); 65 | callback(keys.sort((a, b) => { 66 | return b - a; 67 | })); 68 | }); 69 | } 70 | 71 | getLast(callback) { 72 | this._getAll(contentMap => { 73 | let keys = Object.keys(contentMap); 74 | if (keys && keys.length > 0) { 75 | let max = keys.reduce((p, c) => { 76 | return contentMap[p].updated > contentMap[c].updated ? p : c; 77 | }); 78 | let data = contentMap[max]; 79 | callback(new Work(data.created, data.content, data.updated)); 80 | } else { 81 | callback(null); 82 | } 83 | }); 84 | } 85 | 86 | removeAll(callback) { 87 | localStorage.removeItem('contentMap'); 88 | if (callback) { 89 | callback(); 90 | } 91 | } 92 | 93 | remove(work, callback) { 94 | this._getAll(contentMap => { 95 | delete contentMap[work.created]; 96 | localStorage.setItem('contentMap', JSON.stringify(contentMap)); 97 | if (callback) { 98 | callback(); 99 | } 100 | }); 101 | } 102 | 103 | // Private functions 104 | 105 | _getAll(callback) { 106 | let contentMap = localStorage.getItem('contentMap'); 107 | callback(contentMap ? JSON.parse(contentMap) : {}); 108 | } 109 | 110 | } -------------------------------------------------------------------------------- /app/scripts/mindmap.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import Bounds from './bounds.js'; 4 | import Node from './node.js'; 5 | 6 | const DEFAULT_TEXT_FONT_SIZE = 14; 7 | const TEXT_FONT_FAMILY = 'sans-serif'; 8 | const NODE_MARGIN_WIDTH = 25; 9 | const NODE_MARGIN_HEIGHT = 15; 10 | const NODE_LINE_MARGIN = 5; 11 | const CENTER_NODE_MARGIN = 5; 12 | const LINE_COLORS = ['#0000AA', '#00AA00', '#00AAAA', '#AA0000', '#AA00AA', '#AAAA00']; 13 | const DEFAULT_LINE_COLOR = 'gray'; 14 | 15 | export default class MindMap { 16 | 17 | constructor(newtab, targetElementId) { 18 | this.newtab = newtab; 19 | $.jCanvas.defaults.fromCenter = false; 20 | $.jCanvas.defaults.layer = true; 21 | this.canvasDom_ = document.querySelector(targetElementId); 22 | this._setupCanvasMoving(); 23 | this.canvas_ = $(this.canvasDom_); 24 | this._initializeCanvas(); 25 | } 26 | 27 | // Public functions 28 | 29 | draw(root) { 30 | this.clear(); 31 | 32 | this._adjustFontSize(); 33 | 34 | this._setNodeId(root); 35 | 36 | let leftChildren = null, rightChildren = null; 37 | [leftChildren, rightChildren] = this._divideBalancedNodes(root); 38 | 39 | let leftMaxTextLengthNodes = this._getMaxTextLengthNodes(leftChildren); 40 | let rightMaxTextLengthNodes = this._getMaxTextLengthNodes(rightChildren); 41 | 42 | let leftLeafCount = this._getAllLeafCount(leftChildren); 43 | let rightLeafCount = this._getAllLeafCount(rightChildren); 44 | 45 | let canvasSize = this._getCanvasSize(root, leftLeafCount, rightLeafCount, leftMaxTextLengthNodes, rightMaxTextLengthNodes); 46 | this._drawBackgroundColor(canvasSize); 47 | 48 | let centerNodeBounds = this._drawCenterNode( 49 | canvasSize.leftNodesWidth, 50 | Math.max(canvasSize.height / 2 - this._getFontSize() - CENTER_NODE_MARGIN, 0), 51 | root.id, 52 | root.text, 53 | root.position); 54 | 55 | let leftHeightMargin = 0; 56 | let rightHeightMargin = 0; 57 | if (canvasSize.leftHeight < canvasSize.rightHeight) { 58 | leftHeightMargin = (canvasSize.rightHeight - canvasSize.leftHeight) / 2; 59 | } else { 60 | rightHeightMargin = (canvasSize.leftHeight - canvasSize.rightHeight) / 2; 61 | } 62 | this._drawLeftNodeChildrenFromCenterNode(leftChildren, centerNodeBounds, leftHeightMargin); 63 | this._drawRightNodeChildrenFromCenterNode(rightChildren, centerNodeBounds, rightHeightMargin); 64 | 65 | this._adjustCanvasSize(canvasSize); 66 | } 67 | 68 | clear() { 69 | this.canvas_.removeLayers(); 70 | this.canvas_.drawLayers(); 71 | this._resetLineColorIndex(); 72 | } 73 | 74 | saveAsImage(title, format) { 75 | this.canvasDom_.toBlob(blob => { 76 | const anchor = document.createElement('a'); 77 | const url = window.URL.createObjectURL(blob); 78 | anchor.href = url; 79 | anchor.target = '_blank'; 80 | anchor.download = title + '.' + format; 81 | anchor.click(); 82 | }, 'image/' + format); 83 | } 84 | 85 | changeLineColorMode(state) { 86 | localStorage.lineColorMode = state; 87 | } 88 | 89 | // Private functions 90 | 91 | _getFontSize() { 92 | return Number(localStorage.fontSize || DEFAULT_TEXT_FONT_SIZE); 93 | } 94 | 95 | _adjustFontSize() { 96 | this.canvasDom_.style.fontSize = this._getFontSize() + 'px'; 97 | this.canvasDom_.getContext('2d').font = this._getFontSize() + 'px ' + TEXT_FONT_FAMILY; 98 | } 99 | 100 | _getNodeHeight() { 101 | return this._getFontSize() + NODE_LINE_MARGIN + 1; 102 | } 103 | 104 | _getNodeHeightWithMargin() { 105 | return this._getNodeHeight() + NODE_MARGIN_HEIGHT; 106 | } 107 | 108 | _resetLineColorIndex() { 109 | this.lineColorIndex = 0; 110 | } 111 | 112 | _changeLineColorIndex() { 113 | this.lineColorIndex += 1; 114 | if (LINE_COLORS.length <= this.lineColorIndex) { 115 | this.lineColorIndex = 0; 116 | } 117 | } 118 | 119 | _getLineColor() { 120 | const lineColorMode = JSON.parse(localStorage.lineColorMode || 'false'); 121 | if (lineColorMode) { 122 | return LINE_COLORS[this.lineColorIndex]; 123 | } else { 124 | return DEFAULT_LINE_COLOR; 125 | } 126 | } 127 | 128 | _setupCanvasMoving() { 129 | let x, y, sx, sy, dragging; 130 | this.canvasDom_.addEventListener('mousedown', e => { 131 | x = e.pageX; 132 | y = e.pageY; 133 | sx = this.canvasDom_.parentNode.scrollLeft; 134 | sy = this.canvasDom_.parentNode.scrollTop; 135 | dragging = true; 136 | this.canvasDom_.style.cursor = 'move'; 137 | }); 138 | this.canvasDom_.addEventListener('mousemove', e => { 139 | if (dragging) { 140 | this.canvasDom_.parentNode.scrollLeft = sx - (e.pageX - x); 141 | this.canvasDom_.parentNode.scrollTop = sy - (e.pageY - y); 142 | } 143 | }); 144 | this.canvasDom_.addEventListener('mouseup', () => { 145 | dragging = false; 146 | this.canvasDom_.style.cursor = 'default'; 147 | }); 148 | this.canvasDom_.addEventListener('mouseleave', () => { 149 | dragging = false; 150 | this.canvasDom_.style.cursor = 'default'; 151 | }); 152 | this.canvasDom_.addEventListener('touchstart', e => { 153 | x = e.changedTouches[0].pageX; 154 | y = e.changedTouches[0].pageY; 155 | sx = this.canvasDom_.parentNode.scrollLeft; 156 | sy = this.canvasDom_.parentNode.scrollTop; 157 | dragging = true; 158 | this.canvasDom_.style.cursor = 'move'; 159 | }); 160 | this.canvasDom_.addEventListener('touchmove', e => { 161 | if (dragging) { 162 | this.canvasDom_.parentNode.scrollLeft = sx - (e.changedTouches[0].pageX - x); 163 | this.canvasDom_.parentNode.scrollTop = sy - (e.changedTouches[0].pageY - y); 164 | } 165 | }); 166 | this.canvasDom_.addEventListener('touchend', () => { 167 | dragging = false; 168 | this.canvasDom_.style.cursor = 'default'; 169 | }); 170 | } 171 | 172 | _setNodeId(node) { 173 | let id = 0; 174 | Node.visit(node, x => { 175 | x.id = id; 176 | id += 1; 177 | }); 178 | } 179 | 180 | _adjustCanvasSize(canvasSize) { 181 | this.canvas_.attr('width', canvasSize.width); 182 | this.canvas_.attr('height', canvasSize.height); 183 | this.canvas_.drawLayers(); 184 | } 185 | 186 | _sum(x) { 187 | if (x.length > 0) { 188 | return x.reduce((p, c) => { 189 | return p + c; 190 | }); 191 | } else { 192 | return 0; 193 | } 194 | } 195 | 196 | _measureText(text) { 197 | const context = this.canvasDom_.getContext('2d'); 198 | return context.measureText(text).width; 199 | } 200 | 201 | _drawText(x, y, name, text, position, bold, strikeThrough) { 202 | this.canvas_.drawText({ 203 | fillStyle: strikeThrough ? 'lightgray' : bold ? 'red' : 'black', 204 | // strokeStyle: 'black', 205 | strokeWidth: '0', 206 | x: x, 207 | y: y, 208 | fontSize: this._getFontSize(), 209 | fontFamily: TEXT_FONT_FAMILY, 210 | text: text, 211 | name: name + '-text', 212 | click: (position => { 213 | return () => { 214 | this.newtab.jumpCaretTo(position); 215 | }; 216 | })(position) 217 | }); 218 | return this.canvas_.getLayer(name + '-text'); 219 | } 220 | 221 | _drawLink(x, y, name, text, url) { 222 | this.canvas_.drawText({ 223 | fillStyle: 'blue', 224 | // strokeStyle: 'black', 225 | strokeWidth: '0', 226 | x: x, 227 | y: y, 228 | fontSize: this._getFontSize(), 229 | fontFamily: TEXT_FONT_FAMILY, 230 | text: text, 231 | name: name + '-text', 232 | click: (url => { 233 | return (layout) => { 234 | if (layout.event.shiftKey) { 235 | window.open(url); 236 | } else { 237 | location.href = url; 238 | } 239 | }; 240 | })(url), 241 | cursors: { 242 | mouseover: 'pointer' 243 | } 244 | }); 245 | return this.canvas_.getLayer(name + '-text'); 246 | } 247 | 248 | _initializeCanvas() { 249 | let dummyTextLayer = this._drawText(0, 0, 'dummy', '', 0, false, false); 250 | this.canvas_.removeLayer(dummyTextLayer).drawLayers(); 251 | } 252 | 253 | _drawRect(x, y, width, height, name) { 254 | this.canvas_.drawRect({ 255 | strokeStyle: 'gray', 256 | strokeWidth: 1, 257 | x: x, 258 | y: y, 259 | width: width, 260 | height: height, 261 | cornerRadius: 5, 262 | name: name + '-rect', 263 | intangible: true 264 | }); 265 | return this.canvas_.getLayer(name + '-rect'); 266 | } 267 | 268 | _drawLine(x1, y1, x2, y2, name) { 269 | this.canvas_.drawLine({ 270 | strokeStyle: this._getLineColor(), 271 | strokeWidth: 1, 272 | x1: x1, y1: y1, 273 | x2: x2, y2: y2, 274 | name: name + '-line' 275 | }); 276 | return this.canvas_.getLayer(name + '-line'); 277 | } 278 | 279 | _drawCenterNode(x, y, name, text, position) { 280 | let textLayer = this._drawText(x + CENTER_NODE_MARGIN, y + CENTER_NODE_MARGIN, name, text, position, false, false); 281 | let width = textLayer.width + CENTER_NODE_MARGIN * 2; 282 | let height = textLayer.height + CENTER_NODE_MARGIN * 2; 283 | this._drawRect(x, y, width, height, name); 284 | return new Bounds(x, y, width, height); 285 | } 286 | 287 | _drawNode(x, y, isLeftBase, node) { 288 | let textWidth = this._measureText(node.text); 289 | if (isLeftBase) { 290 | // this._drawText(x, y, node.id, node.text); 291 | this._drawTokens(x, y, node); 292 | this._drawLine(x, y + this._getFontSize() + NODE_LINE_MARGIN, x + textWidth, y + this._getFontSize() + NODE_LINE_MARGIN, node.id); 293 | return new Bounds(x, y, textWidth, this._getFontSize() + NODE_LINE_MARGIN); 294 | } else { 295 | // this._drawText(x - textWidth, y, node.id, node.text); 296 | this._drawTokens(x - textWidth, y, node); 297 | this._drawLine(x - textWidth, y + this._getFontSize() + NODE_LINE_MARGIN, x, y + this._getFontSize() + NODE_LINE_MARGIN, node.id); 298 | return new Bounds(x - textWidth, y, textWidth, this._getFontSize() + NODE_LINE_MARGIN); 299 | } 300 | } 301 | 302 | _drawTokens(x, y, node) { 303 | let cx = x; 304 | node.tokens.forEach((token, index) => { 305 | let layer; 306 | if (token.hasUrl()) { 307 | layer = this._drawLink(cx, y, node.id + '-' + index, token.text, token.url); 308 | } else { 309 | layer = this._drawText(cx, y, node.id + '-' + index, token.text, node.position, token.isBold(), token.isStrikeThrough()); 310 | } 311 | cx = cx + layer.width; 312 | }); 313 | } 314 | 315 | _connectNodeToCenterNode(nodeBounds, centerNodeBounds, name) { 316 | let isLeft = nodeBounds.x2 < centerNodeBounds.x1; 317 | let x1 = isLeft ? nodeBounds.x2 : nodeBounds.x1; 318 | let y1 = nodeBounds.y1 + nodeBounds.height; 319 | let cx1 = isLeft ? centerNodeBounds.x1 : centerNodeBounds.x1 + centerNodeBounds.width; 320 | let cy1 = nodeBounds.y1 + nodeBounds.height; 321 | let cx2 = isLeft ? nodeBounds.x2 : nodeBounds.x1; 322 | let cy2 = centerNodeBounds.y1 + centerNodeBounds.height / 2; 323 | let x2 = isLeft ? centerNodeBounds.x1 : centerNodeBounds.x2; 324 | let y2 = centerNodeBounds.y1 + centerNodeBounds.height / 2; 325 | this.canvas_.drawBezier({ 326 | strokeStyle: this._getLineColor(), 327 | strokeWidth: 1, 328 | x1: x1, y1: y1, 329 | cx1: cx1, cy1: cy1, 330 | cx2: cx2, cy2: cy2, 331 | x2: x2, y2: y2, 332 | name: name + '-bezier' 333 | }); 334 | } 335 | 336 | _connectNodes(parentBounds, childBounds, name) { 337 | let isLeft = childBounds.x2 < parentBounds.x1; 338 | let x1 = isLeft ? childBounds.x2 : childBounds.x1; 339 | let y1 = childBounds.y1 + childBounds.height; 340 | let cx1 = isLeft ? parentBounds.x1 : parentBounds.x1 + parentBounds.width; 341 | let cy1 = childBounds.y1 + childBounds.height; 342 | let cx2 = isLeft ? childBounds.x2 : childBounds.x1; 343 | let cy2 = parentBounds.y2; 344 | let x2 = isLeft ? parentBounds.x1 : parentBounds.x2; 345 | let y2 = parentBounds.y2; 346 | this.canvas_.drawBezier({ 347 | strokeStyle: this._getLineColor(), 348 | strokeWidth: 1, 349 | x1: x1, y1: y1, 350 | cx1: cx1, cy1: cy1, 351 | cx2: cx2, cy2: cy2, 352 | x2: x2, y2: y2, 353 | name: name + '-bezier' 354 | }); 355 | } 356 | 357 | _getLeafCount(node) { 358 | let leafCount = 0; 359 | Node.visit(node, x => { 360 | if (x.isLeaf()) { 361 | leafCount += 1; 362 | } 363 | }); 364 | return leafCount; 365 | } 366 | 367 | _getDivideIndex(root) { 368 | let leafCountList = root.children.map(child => { 369 | return this._getLeafCount(child); 370 | }); 371 | let minDelta = null; 372 | let divideIndex = null; 373 | leafCountList.forEach((leafCount, index) => { 374 | if (index < leafCountList.length - 1) { 375 | let sum = (prev, current) => { 376 | return prev + current; 377 | }; 378 | let leftSum = leafCountList.slice(0, index + 1).reduce(sum); 379 | let rightSum = leafCountList.slice(index + 1).reduce(sum); 380 | let delta = Math.abs(leftSum - rightSum); 381 | if (minDelta !== null) { 382 | if (delta < minDelta) { 383 | minDelta = delta; 384 | divideIndex = index; 385 | } 386 | } else { 387 | minDelta = delta; 388 | divideIndex = index; 389 | } 390 | } 391 | }); 392 | return divideIndex; 393 | } 394 | 395 | _divideBalancedNodes(root) { 396 | const wingMode = localStorage.wingMode || 'both'; 397 | if (wingMode === 'both') { 398 | let divideIndex = this._getDivideIndex(root); 399 | let left = root.children.slice(0, divideIndex + 1); 400 | let right = root.children.slice(divideIndex + 1); 401 | return [left, right]; 402 | } else if (wingMode === 'right') { 403 | return [[], root.children]; 404 | } else { 405 | return [root.children, []]; 406 | } 407 | } 408 | 409 | _getAllTextLength(nodes) { 410 | if (nodes.length > 0) { 411 | let sizes = nodes.map(x => { 412 | return this._measureText(x.text); 413 | }); 414 | sizes.push((sizes.length - 1) * NODE_MARGIN_WIDTH); 415 | return this._sum(sizes); 416 | } else { 417 | return 0; 418 | } 419 | } 420 | 421 | _getMaxTextLengthNodes(children) { 422 | let maxLengthNodes = []; 423 | let traverse = (children, prev) => { 424 | children.forEach(node => { 425 | let nodes = prev.slice(); 426 | nodes.push(node); 427 | if (node.isLeaf()) { 428 | if (this._getAllTextLength(maxLengthNodes) < this._getAllTextLength(nodes)) { 429 | maxLengthNodes = nodes; 430 | } 431 | } else { 432 | traverse(node.children, nodes); 433 | } 434 | }); 435 | }; 436 | traverse(children, []); 437 | return maxLengthNodes; 438 | } 439 | 440 | _getAllLeafCount(nodes) { 441 | return this._sum(nodes.map(node => { 442 | return this._getLeafCount(node); 443 | })); 444 | } 445 | 446 | _getCanvasSize(root, leftLeafCount, rightLeafCount, leftMaxTextLengthNodes, rightMaxTextLengthNodes) { 447 | const getWidth = nodes => { 448 | return this._sum(nodes.map(node => { 449 | return this._measureText(node.text) + NODE_MARGIN_WIDTH; 450 | })); 451 | }; 452 | const leftNodesWidth = getWidth(leftMaxTextLengthNodes); 453 | const rightNodesWidth = getWidth(rightMaxTextLengthNodes); 454 | const centerNodeWidth = this._measureText(root.text) + CENTER_NODE_MARGIN * 2 + 1; 455 | const width = leftNodesWidth + rightNodesWidth + centerNodeWidth; 456 | let height = Math.max(this._getNodeHeightWithMargin() * Math.max(leftLeafCount, rightLeafCount), this._getFontSize() + CENTER_NODE_MARGIN * 2); 457 | const leftHeight = this._getNodeHeightWithMargin() * leftLeafCount; 458 | const rightHeight = this._getNodeHeightWithMargin() * rightLeafCount; 459 | return { 460 | width: width, 461 | height: height, 462 | leftNodesWidth: leftNodesWidth, 463 | rightNodesWidth: rightNodesWidth, 464 | centerNodeWidth: centerNodeWidth, 465 | leftHeight: leftHeight, 466 | rightHeight: rightHeight 467 | }; 468 | } 469 | 470 | _drawLeftNodeChildrenFromNode(children, parentNodeBounds, baseHeight) { 471 | let currentHeight = baseHeight; 472 | children.forEach(node => { 473 | let allNodesHeight = this._getLeafCount(node) * this._getNodeHeightWithMargin(); 474 | let y = currentHeight + allNodesHeight / 2 - this._getNodeHeight() / 2; 475 | let x = parentNodeBounds.x - NODE_MARGIN_WIDTH; 476 | let nodeBounds = this._drawNode(x, y, false, node); 477 | this._connectNodes(parentNodeBounds, nodeBounds, node.id); 478 | this._drawLeftNodeChildrenFromNode(node.children, nodeBounds, currentHeight); 479 | currentHeight += allNodesHeight; 480 | }); 481 | } 482 | 483 | _drawLeftNodeChildrenFromCenterNode(children, centerNodeBounds, topMargin) { 484 | let currentHeight = topMargin; 485 | children.forEach(node => { 486 | let allNodesHeight = this._getLeafCount(node) * this._getNodeHeightWithMargin(); 487 | let y = currentHeight + allNodesHeight / 2 - this._getNodeHeight() / 2; 488 | let x = centerNodeBounds.x - NODE_MARGIN_WIDTH; 489 | let nodeBounds = this._drawNode(x, y, false, node); 490 | this._connectNodeToCenterNode(nodeBounds, centerNodeBounds, node.id); 491 | this._drawLeftNodeChildrenFromNode(node.children, nodeBounds, currentHeight); 492 | currentHeight += allNodesHeight; 493 | this._changeLineColorIndex(); 494 | }); 495 | } 496 | 497 | _drawRightNodeChildrenFromNode(children, parentNodeBounds, baseHeight) { 498 | let currentHeight = baseHeight; 499 | children.forEach(node => { 500 | let allNodesHeight = this._getLeafCount(node) * this._getNodeHeightWithMargin(); 501 | let y = currentHeight + allNodesHeight / 2 - this._getNodeHeight() / 2; 502 | let x = parentNodeBounds.x2 + NODE_MARGIN_WIDTH; 503 | let nodeBounds = this._drawNode(x, y, true, node); 504 | this._connectNodes(parentNodeBounds, nodeBounds, node.id); 505 | this._drawRightNodeChildrenFromNode(node.children, nodeBounds, currentHeight); 506 | currentHeight += allNodesHeight; 507 | }); 508 | } 509 | 510 | _drawRightNodeChildrenFromCenterNode(children, centerNodeBounds, topMargin) { 511 | let currentHeight = topMargin; 512 | children.forEach(node => { 513 | let allNodesHeight = this._getLeafCount(node) * this._getNodeHeightWithMargin(); 514 | let y = currentHeight + allNodesHeight / 2 - this._getNodeHeight() / 2; 515 | let x = centerNodeBounds.x2 + NODE_MARGIN_WIDTH; 516 | let nodeBounds = this._drawNode(x, y, true, node); 517 | this._connectNodeToCenterNode(nodeBounds, centerNodeBounds, node.id); 518 | this._drawRightNodeChildrenFromNode(node.children, nodeBounds, currentHeight); 519 | currentHeight += allNodesHeight; 520 | this._changeLineColorIndex(); 521 | }); 522 | } 523 | 524 | _drawBackgroundColor(canvasSize) { 525 | this.canvas_.drawRect({ 526 | fillStyle: 'white', 527 | x: 0, 528 | y: 0, 529 | width: canvasSize.width, 530 | height: canvasSize.height 531 | }); 532 | } 533 | 534 | } 535 | -------------------------------------------------------------------------------- /app/scripts/newtab.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import MindMap from './mindmap.js'; 4 | import Parser from './parser.js'; 5 | import Work from './work.js'; 6 | import Node from './node.js'; 7 | import ChromeWorkStorage from './chrome_work_storage.js'; 8 | import FirebaseWorkStorage from './firebase_work_storage.js'; 9 | import LocalWorkStorage from './local_work_storage.js'; 10 | 11 | class Newtab { 12 | 13 | constructor() { 14 | this.useFirebase = false; 15 | this.typing = false; 16 | this.loading = false; 17 | this.timer = false; 18 | 19 | this.showStatusMessage('Initialiing...'); 20 | 21 | this.chromeWorkStorage = new ChromeWorkStorage(this); 22 | this.firebaseWorkStorage = new FirebaseWorkStorage(this); 23 | this.localWorkStorage = new LocalWorkStorage(this); 24 | this.localWorkStorage.initialize(() => { 25 | this.chromeWorkStorage.initialize(() => { 26 | this.firebaseWorkStorage.initialize(alreadyLoggedIn => { 27 | this.mm = new MindMap(this, '#target'); 28 | this.currentWork = Work.newInstance(); 29 | this.editor = this.initializeAceEditor(); 30 | this.calendar = this.initializeCalendar(); 31 | this.changeUseFirebase(alreadyLoggedIn); 32 | this.setConfigrationToUI(); 33 | this.assignEventHandlers(); 34 | this.loadWorkList(works => { 35 | const showLastMindmapMode = JSON.parse(localStorage.showLastMindmapMode || 'false'); 36 | if (showLastMindmapMode) { 37 | const lastMindmapId = localStorage.lastLoaded; 38 | if (lastMindmapId) { 39 | for (let work of works) { 40 | if (lastMindmapId == work.created) { 41 | this.load(work); 42 | break; 43 | } 44 | } 45 | } 46 | } 47 | }); 48 | this.changeLayoutVisibility(); 49 | this.showStatusMessage('Initialized.'); 50 | }); 51 | }); 52 | }); 53 | } 54 | 55 | // Ace Editor 56 | 57 | initializeAceEditor() { 58 | this.showStatusMessage('Initializing Ace Editor.'); 59 | 60 | let editor = ace.edit('source'); 61 | // editor.setFontSize(13); 62 | editor.setOptions({ 63 | fontFamily: 'MeiryoKe_Gothic, \'Courier New\', Courier, Monaco, Mento, monospace', 64 | fontSize: '14px' 65 | }); 66 | editor.setDisplayIndentGuides(true); 67 | editor.getSession().setTabSize(4); 68 | editor.getSession().setUseSoftTabs(false); 69 | editor.getSession().setUseWrapMode(false); 70 | editor.setShowPrintMargin(false); 71 | editor.setHighlightActiveLine(true); 72 | editor.renderer.setShowGutter(true); 73 | editor.$blockScrolling = Infinity; 74 | this.showStatusMessage('Initialized Ace Editor.'); 75 | 76 | return editor; 77 | } 78 | 79 | // Calendar 80 | 81 | initializeCalendar() { 82 | return $('#calendar').fullCalendar({ 83 | defaultView: 'month', 84 | displayEventTime: false, 85 | eventTextColor: 'white', 86 | eventBackgroundColor: '#2196F3', 87 | eventRender: (event, element) => { 88 | element.attr('title', event.title); 89 | } 90 | }).fullCalendar('getCalendar'); 91 | } 92 | 93 | // Event Handlers 94 | 95 | assignEventHandlers() { 96 | this.showStatusMessage('Assigning event handlers.'); 97 | 98 | this.editor.getSession().on('change', () => { 99 | this.onEditorSessionChanged(); 100 | }); 101 | this.editor.getSession().getSelection().on('changeSelection', () => { 102 | this.onEditorSelectionChanged(); 103 | }); 104 | 105 | ['btnDelete', 'btnConfirmYes', 'btnCalendar', 106 | 'btnCopyAsPlainText', 'btnCopyAsMarkdownText', 'btnOnline', 107 | 'btnLogin', 'btnOpenCreateUserDialog', 'btnCreateUser', 108 | 'btnForgotPassword', 'btnExportAsPng', 'btnExportAsJpeg', 109 | 'btnLayoutRightMain', 'btnLayoutLeftMain', 'btnLayoutRightOnly', 110 | 'btnLayoutLeftOnly', 'btnCopyAsHtmlText', 'btnLineColorModeOn', 111 | 'btnLineColorModeOff', 'btnEditBold', 'btnEditStrikeThrough', 112 | 'btnFilterStrikeThroughTextModeOn', 'btnFilterStrikeThroughTextModeOff', 113 | 'btnWingModeBoth', 'btnWingModeLeftOnly', 'btnWingModeRightOnly', 114 | 'btnShowLastMindmapModeOn', 'btnShowLastMindmapModeOff'].forEach(name => { 115 | let element = document.querySelector('#' + name); 116 | element.addEventListener('click', () => { 117 | this.hideNavbar(); 118 | this['on' + name.charAt(0).toUpperCase() + name.slice(1) + 'Clicked'](); 119 | }); 120 | }); 121 | 122 | ['footerBtnLayoutRightMain', 'footerBtnLayoutLeftMain', 'footerBtnLayoutRightOnly', 123 | 'footerBtnLayoutLeftOnly', 'footerBtnCalendar'].forEach(name => { 124 | let element = document.querySelector('#' + name); 125 | element.addEventListener('click', () => { 126 | this.hideNavbar(); 127 | this['on' + name.charAt(6).toUpperCase() + name.slice(7) + 'Clicked'](); 128 | }); 129 | }); 130 | 131 | [10, 12, 14, 16, 18, 24, 36].forEach(fontSize => { 132 | let element = document.querySelector('#btnFontSize' + fontSize); 133 | element.addEventListener('click', ((fontSize) => { 134 | return () => { 135 | this.hideNavbar(); 136 | localStorage.fontSize = fontSize; 137 | this.drawMindmap(); 138 | }; 139 | })(fontSize)); 140 | }); 141 | 142 | $('#loginDialog').on('shown.bs.modal', () => { 143 | $('#inputEmail').focus(); 144 | }); 145 | 146 | $('#createUserDialog').on('shown.bs.modal', () => { 147 | $('#inputNewEmail').focus(); 148 | }); 149 | 150 | const visibilityDivs = { 151 | 'xs': $('
'), 152 | 'sm': $('
'), 153 | 'md': $('
'), 154 | 'lg': $('
'), 155 | 'xl': $('
') 156 | }; 157 | 158 | ResponsiveBootstrapToolkit.use('custom', visibilityDivs); 159 | 160 | $(window).resize(ResponsiveBootstrapToolkit.changed(() => { 161 | this.changeLayoutVisibility(); 162 | })); 163 | 164 | this.showStatusMessage('Assigned event handlers.'); 165 | } 166 | 167 | changeLayoutVisibility() { 168 | if (ResponsiveBootstrapToolkit.is('<=md')) { 169 | $('#btnLayoutRightMain').hide(); 170 | $('#btnLayoutLeftMain').hide(); 171 | $('#footerBtnLayoutRightMain').hide(); 172 | $('#footerBtnLayoutLeftMain').hide(); 173 | const leftColumn = document.querySelector('#leftColumn'); 174 | const rightColumn = document.querySelector('#rightColumn'); 175 | const leftClassName = leftColumn.getAttribute('class'); 176 | if (leftClassName.includes('-8') || leftClassName.includes('-12')) { 177 | leftColumn.setAttribute('class', 'd-block col-lg-12'); 178 | rightColumn.setAttribute('class', 'd-none'); 179 | } else { 180 | leftColumn.setAttribute('class', 'd-none'); 181 | rightColumn.setAttribute('class', 'd-block col-lg-12'); 182 | } 183 | } else { 184 | $('#btnLayoutRightMain').show(); 185 | $('#btnLayoutLeftMain').show(); 186 | $('#footerBtnLayoutRightMain').show(); 187 | $('#footerBtnLayoutLeftMain').show(); 188 | } 189 | } 190 | 191 | onBtnEditBoldClicked() { 192 | const selectionRange = this.editor.getSelectionRange(); 193 | const textRange = this.editor.getSession().getTextRange(selectionRange); 194 | if (textRange.length > 0) { 195 | this.editor.getSession().replace(selectionRange, `**${textRange}**`); 196 | } 197 | } 198 | 199 | onBtnEditStrikeThroughClicked() { 200 | const selectionRange = this.editor.getSelectionRange(); 201 | const textRange = this.editor.getSession().getTextRange(selectionRange); 202 | if (textRange.length > 0) { 203 | this.editor.getSession().replace(selectionRange, `~~${textRange}~~`); 204 | } 205 | } 206 | 207 | onBtnLineColorModeOnClicked() { 208 | this.mm.changeLineColorMode(true); 209 | this.drawMindmap(); 210 | } 211 | 212 | onBtnLineColorModeOffClicked() { 213 | this.mm.changeLineColorMode(false); 214 | this.drawMindmap(); 215 | } 216 | 217 | onBtnShowLastMindmapModeOnClicked() { 218 | localStorage.showLastMindmapMode = true; 219 | } 220 | 221 | onBtnShowLastMindmapModeOffClicked() { 222 | localStorage.showLastMindmapMode = false; 223 | } 224 | 225 | onBtnFilterStrikeThroughTextModeOnClicked() { 226 | localStorage.filterStrikeThrough = false; 227 | this.drawMindmap(); 228 | } 229 | 230 | onBtnFilterStrikeThroughTextModeOffClicked() { 231 | localStorage.filterStrikeThrough = true; 232 | this.drawMindmap(); 233 | } 234 | 235 | onBtnLayoutRightMainClicked() { 236 | let leftColumn = document.querySelector('#leftColumn'); 237 | let rightColumn = document.querySelector('#rightColumn'); 238 | leftColumn.setAttribute('class', 'd-block col-lg-4'); 239 | rightColumn.setAttribute('class', 'd-block col-lg-8'); 240 | } 241 | 242 | onBtnLayoutLeftMainClicked() { 243 | let leftColumn = document.querySelector('#leftColumn'); 244 | let rightColumn = document.querySelector('#rightColumn'); 245 | leftColumn.setAttribute('class', 'd-block col-lg-8'); 246 | rightColumn.setAttribute('class', 'd-block col-lg-4'); 247 | } 248 | 249 | onBtnLayoutRightOnlyClicked() { 250 | let leftColumn = document.querySelector('#leftColumn'); 251 | let rightColumn = document.querySelector('#rightColumn'); 252 | leftColumn.setAttribute('class', 'd-none'); 253 | rightColumn.setAttribute('class', 'd-block col-lg-12'); 254 | } 255 | 256 | onBtnLayoutLeftOnlyClicked() { 257 | let leftColumn = document.querySelector('#leftColumn'); 258 | let rightColumn = document.querySelector('#rightColumn'); 259 | leftColumn.setAttribute('class', 'd-block col-lg-12'); 260 | rightColumn.setAttribute('class', 'd-none'); 261 | } 262 | 263 | onEditorSessionChanged() { 264 | if (this.timer !== false) { 265 | clearTimeout(this.timer); 266 | } 267 | this.timer = setTimeout(() => { 268 | this.showStatusMessage('Editor session changed.'); 269 | this.typing = true; 270 | this.drawMindmap(() => { 271 | if (!this.loading) { 272 | this.showStatusMessage('Saving the content.'); 273 | this.getWorkStorage().save(this.currentWork, () => { 274 | this.showStatusMessage('Saved the content.'); 275 | this.loadWorkList(() => { 276 | this.timer = false; 277 | this.showStatusMessage('Saved and reloaded.'); 278 | }); 279 | }); 280 | } 281 | }); 282 | }, 2000); 283 | } 284 | 285 | onEditorSelectionChanged() { 286 | const selectionRange = this.editor.getSelectionRange(); 287 | const textRange = this.editor.getSession().getTextRange(selectionRange); 288 | if (textRange.length > 0) { 289 | $('.dropdownEditItem').removeClass('disabled'); 290 | } else { 291 | $('.dropdownEditItem').addClass('disabled'); 292 | } 293 | } 294 | 295 | onBtnLastClicked() { 296 | this.getWorkStorage().getLast(work => { 297 | this.load(work); 298 | this.showStatusMessage('Last mindmap loaded.'); 299 | }); 300 | } 301 | 302 | onBtnDeleteClicked() { 303 | if (this.currentWork.content) { 304 | let confirmMessage = document.querySelector('#confirmMessage'); 305 | confirmMessage.innerText = 'Do you really want to delete `' + this.currentWork.firstLine + '`?'; 306 | $('#confirmDialog').modal('show'); 307 | } 308 | } 309 | 310 | onBtnConfirmYesClicked() { 311 | $('#confirmDialog').modal('hide'); 312 | if (this.currentWork.content) { 313 | this.showStatusMessage('Removing.'); 314 | 315 | this.getWorkStorage().remove(this.currentWork, () => { 316 | this.loadWorkList(() => { 317 | this.load(Work.newInstance()); 318 | 319 | this.showStatusMessage('Removed and reloaded.'); 320 | }); 321 | }); 322 | } 323 | } 324 | 325 | onBtnNewClicked() { 326 | this.load(Work.newInstance()); 327 | 328 | this.showStatusMessage('New mindmap created.'); 329 | } 330 | 331 | onBtnTopSitesClicked() { 332 | let text = 'Top Sites\n'; 333 | chrome.topSites.get(sites => { 334 | sites.forEach(site => { 335 | text = text + '\t[' + site.title + '](' + site.url + ')\n'; 336 | }); 337 | let work = new Work(Date.now(), text, Date.now()); 338 | work.isSave = false; 339 | this.load(work); 340 | 341 | this.showStatusMessage('Top sites loaded.'); 342 | }); 343 | } 344 | 345 | onBtnHowToUseClicked() { 346 | let text = 'MindMap Tab\n\tAbout\n\t\tCopyright (C) 2017-${year} Yoichiro Tanaka\n\t\tAll rights reserved\n\t\t[GitHub](https://github.com/yoichiro/mindmap_tab)\n\t\t[Issue Tracker](https://github.com/yoichiro/mindmap_tab/issues)\n\t\t[Chrome WebStore](https://chrome.google.com/webstore/detail/mindmap-tab/mkgjficalhplaenklhejcbmlkonbakjj)\n\tHow to Use\n\t\tBasic\n\t\t\tHow to draw Mindmap diagram\n\t\t\t\tWrite an indented text\n\t\t\t\tEach line becomes a node\n\t\t\tHow to indent\n\t\t\t\tWith TAB characters\n\t\t\t\tWith 4 white space characters\n\t\tFormats\n\t\t\t[LINK TITLE](LINK URL)\n\t\t\t\tClick when you want to open\n\t\t\t\tShift+Click when you want to open with new window\n\t\t\t**BOLD**\n\t\t\t~~STRIKE THROUGH~~\n\t\t\t2019/04/24 EVENT\n\t\t\t\tYYYY/MM/DD ...\n\t\t\t\tShown on Calendar View\n'; 347 | text = text.replace('${year}', new Date().getFullYear()); 348 | const work = new Work(Date.now(), text, Date.now()); 349 | work.isSave = false; 350 | this.load(work); 351 | 352 | this.showStatusMessage('How to Use loaded.'); 353 | } 354 | 355 | onBtnExportAsPngClicked() { 356 | if (this.currentWork.hasContent()) { 357 | this.mm.saveAsImage(this.currentWork.firstLine, 'png'); 358 | } 359 | } 360 | 361 | onBtnExportAsJpegClicked() { 362 | if (this.currentWork.hasContent()) { 363 | this.mm.saveAsImage(this.currentWork.firstLine, 'jpeg'); 364 | } 365 | } 366 | 367 | onBtnForgotPasswordClicked() { 368 | this.updateLoginErrorMessage(''); 369 | const email = document.querySelector('#inputEmail').value; 370 | this.firebaseWorkStorage.sendPasswordResetEmail(email, () => { 371 | this.updateLoginErrorMessage('Sent an email to the address.'); 372 | }, error => { 373 | console.error(error); 374 | this.updateLoginErrorMessage(error.message); 375 | }); 376 | } 377 | 378 | onBtnCreateUserClicked() { 379 | this.updateCreateUserErrorMessage(''); 380 | const email = document.querySelector('#inputNewEmail').value; 381 | const password1 = document.querySelector('#inputNewPassword1').value; 382 | const password2 = document.querySelector('#inputNewPassword2').value; 383 | if (password1 && password1 === password2) { 384 | this.firebaseWorkStorage.createUser(email, password1, () => { 385 | this.changeUseFirebase(true); 386 | this.loadWorkList(() => { 387 | this.load(Work.newInstance()); 388 | $('#createUserDialog').modal('hide'); 389 | }); 390 | }, error => { 391 | console.error(error); 392 | this.updateCreateUserErrorMessage(error.message); 393 | }); 394 | } else { 395 | this.updateCreateUserErrorMessage('Invalid password.'); 396 | } 397 | } 398 | 399 | onBtnOpenCreateUserDialogClicked() { 400 | $('#loginDialog').modal('hide'); 401 | this.updateCreateUserErrorMessage(''); 402 | document.querySelector('#inputNewEmail').value = document.querySelector('#inputEmail').value; 403 | document.querySelector('#inputNewPassword1').value = ''; 404 | document.querySelector('#inputNewPassword2').value = ''; 405 | $('#createUserDialog').modal('show'); 406 | } 407 | 408 | onBtnOnlineClicked() { 409 | if (this.useFirebase) { 410 | this.showStatusMessage('Logging out.'); 411 | 412 | this.getWorkStorage().logout(() => { 413 | this.changeUseFirebase(false); 414 | this.loadWorkList(() => { 415 | this.load(Work.newInstance()); 416 | 417 | this.showStatusMessage('Logged out and reloaded.'); 418 | }); 419 | }); 420 | } else { 421 | document.querySelector('#inputPassword').value = ''; 422 | this.updateLoginErrorMessage(''); 423 | $('#loginDialog').modal('show'); 424 | } 425 | } 426 | 427 | onBtnLoginClicked() { 428 | this.updateLoginErrorMessage(''); 429 | const email = document.querySelector('#inputEmail').value; 430 | const passwd = document.querySelector('#inputPassword').value; 431 | 432 | this.showStatusMessage('Logging in.'); 433 | 434 | this.firebaseWorkStorage.login(email, passwd, () => { 435 | this.changeUseFirebase(true); 436 | this.loadWorkList(() => { 437 | this.load(Work.newInstance()); 438 | $('#loginDialog').modal('hide'); 439 | 440 | this.showStatusMessage('Logged in.'); 441 | }); 442 | }, error => { 443 | console.error(error); 444 | this.updateLoginErrorMessage(error.message); 445 | }); 446 | } 447 | 448 | onBtnCopyAsPlainTextClicked() { 449 | let source = this.editor.getValue(); 450 | if (source) { 451 | this.copyTextToClipboardViaCopyBuffer(source); 452 | } 453 | } 454 | 455 | onBtnCopyAsMarkdownTextClicked() { 456 | let source = this.editor.getValue(); 457 | let root = new Parser().parse(source, this.isFilterStrikeThroughText()); 458 | if (root) { 459 | let text = ''; 460 | let traverse = (node, currentLevel) => { 461 | for (let i = 0; i < currentLevel; i += 1) { 462 | text += ' '; 463 | } 464 | text += '* ' + node.source + '\n'; 465 | if (!node.isLeaf()) { 466 | node.children.forEach(child => { 467 | traverse(child, currentLevel + 1); 468 | }); 469 | } 470 | }; 471 | traverse(root, 0); 472 | this.copyTextToClipboardViaCopyBuffer(text); 473 | } 474 | } 475 | 476 | onBtnCopyAsHtmlTextClicked() { 477 | let source = this.editor.getValue(); 478 | let root = new Parser().parse(source, this.isFilterStrikeThroughText()); 479 | if (root) { 480 | let text = ''; 509 | this.copyTextToClipboardViaCopyBuffer(text); 510 | } 511 | } 512 | 513 | onBtnCalendarClicked() { 514 | this.renderEvents(); 515 | $('#calendarDialog').modal('show'); 516 | } 517 | 518 | onBtnWingModeBothClicked() { 519 | localStorage.wingMode = 'both'; 520 | this.drawMindmap(); 521 | } 522 | 523 | onBtnWingModeLeftOnlyClicked() { 524 | localStorage.wingMode = 'left'; 525 | this.drawMindmap(); 526 | } 527 | 528 | onBtnWingModeRightOnlyClicked() { 529 | localStorage.wingMode = 'right'; 530 | this.drawMindmap(); 531 | } 532 | 533 | // Calendar 534 | 535 | renderEvents() { 536 | const source = this.editor.getValue(); 537 | const root = new Parser().parse(source, this.isFilterStrikeThroughText()); 538 | const eventSource = []; 539 | if (root) { 540 | Node.visit(root, node => { 541 | const m = node.source.match(/(\d{4}\/)?\d{1,2}\/\d{1,2}/); 542 | if (m) { 543 | let date = m[0]; 544 | if (date.split('/').length - 1 === 1) { 545 | date = `${new Date().getFullYear()}/${date}`; 546 | } 547 | eventSource.push({ 548 | title: this.createEventTitle(node), 549 | start: new Date(`${date} 00:00:00`) 550 | }); 551 | } 552 | }); 553 | } 554 | this.calendar.removeEventSources(); 555 | this.calendar.addEventSource(eventSource); 556 | } 557 | 558 | createEventTitle(node) { 559 | let target = node; 560 | let result = []; 561 | do { 562 | result.push(target.source.replace(/~/g, '').replace(/\*/g, '')); 563 | target = target.parent; 564 | } while(target !== null && target.parent !== null); 565 | return result.reverse().join(' '); 566 | } 567 | 568 | // Update messages 569 | 570 | updateLoginErrorMessage(message) { 571 | const loginErrorMessage = document.querySelector('#loginErrorMessage'); 572 | if (message) { 573 | loginErrorMessage.innerText = message; 574 | } else { 575 | loginErrorMessage.innerText = ''; 576 | } 577 | } 578 | 579 | updateCreateUserErrorMessage(message) { 580 | const createUserErrorMessage = document.querySelector('#createUserErrorMessage'); 581 | if (message) { 582 | createUserErrorMessage.innerText = message; 583 | } else { 584 | createUserErrorMessage.innerText = ''; 585 | } 586 | } 587 | 588 | // For Firebase 589 | 590 | changeUseFirebase(alreadyLoggedIn) { 591 | this.useFirebase = alreadyLoggedIn; 592 | this.updateBtnOnlineText(); 593 | } 594 | 595 | getWorkStorage() { 596 | if (this.useFirebase) { 597 | return this.firebaseWorkStorage; 598 | } else if (window.chrome !== undefined && chrome.storage !== undefined) { 599 | return this.chromeWorkStorage; 600 | } else { 601 | return this.localWorkStorage; 602 | } 603 | } 604 | 605 | onWorkAdded() { 606 | this.showStatusMessage('Mindmap added message received.'); 607 | this.loadWorkList(() => { 608 | this.typing = false; 609 | 610 | this.showStatusMessage('Handled mindmap added message and reloaded.'); 611 | }); 612 | } 613 | 614 | onWorkChanged(key, changedWork) { 615 | this.showStatusMessage('Mindmap changed message received.'); 616 | this.loadWorkList(() => { 617 | if (!this.typing 618 | && this.currentWork 619 | && this.currentWork.created === changedWork.created 620 | && this.currentWork.content !== changedWork.content) { 621 | this.load(changedWork); 622 | } 623 | this.typing = false; 624 | 625 | this.showStatusMessage('Handled mindmap changed message and reloaded.'); 626 | }); 627 | } 628 | 629 | onWorkRemoved(key, removedWork) { 630 | this.showStatusMessage('Mindmap removed message received.'); 631 | this.loadWorkList(() => { 632 | if (this.currentWork 633 | && this.currentWork.created === removedWork.created) { 634 | this.load(Work.newInstance()); 635 | } 636 | this.typing = false; 637 | 638 | this.showStatusMessage('Handled mindmap removed message and reloaded.'); 639 | }); 640 | } 641 | 642 | updateBtnOnlineText() { 643 | if (this.useFirebase) { 644 | const email = this.firebaseWorkStorage.getCurrentUserEmail(); 645 | document.querySelector('#lblOnline').innerText = 'Logout (' + email + ')'; 646 | } else { 647 | document.querySelector('#lblOnline').innerText = 'Login'; 648 | } 649 | } 650 | 651 | // For Clipboard 652 | 653 | copyTextToClipboardViaCopyBuffer(text) { 654 | const copyBuffer = document.querySelector('#copyBuffer'); 655 | copyBuffer.value = text; 656 | copyBuffer.select(); 657 | try { 658 | const result = document.execCommand('copy'); 659 | const msg = result ? 'successful' : 'unsuccessful'; 660 | console.log('Copy source text was ' + msg); 661 | } catch (e) { 662 | console.log('Oops, unable to copy'); 663 | } 664 | } 665 | 666 | // Draw MindMap 667 | 668 | drawMindmap(callback) { 669 | let source = this.editor.getValue(); 670 | let root = new Parser().parse(source, this.isFilterStrikeThroughText()); 671 | if (root) { 672 | this.currentWork.content = source; 673 | this.mm.draw(root); 674 | } else { 675 | this.mm.clear(); 676 | } 677 | if (callback) { 678 | callback(); 679 | } 680 | } 681 | 682 | appendDividerTo(parent) { 683 | const separator = document.createElement('div'); 684 | separator.setAttribute('class', 'dropdown-divider'); 685 | parent.appendChild(separator); 686 | } 687 | 688 | loadWorkList(callback) { 689 | this.showStatusMessage('Loading mindmaps.'); 690 | 691 | this.getWorkStorage().getAll(works => { 692 | let history = document.querySelector('#history'); 693 | history.innerHTML = ''; 694 | const newLink = document.createElement('a'); 695 | newLink.href = '#'; 696 | newLink.setAttribute('class', 'dropdown-item'); 697 | newLink.appendChild(document.createTextNode('New')); 698 | newLink.addEventListener('click', () => { 699 | this.hideNavbar(); 700 | this.onBtnNewClicked(); 701 | }); 702 | history.appendChild(newLink); 703 | if (works.length > 0) { 704 | this.appendDividerTo(history); 705 | } 706 | works.forEach(work => { 707 | const link = document.createElement('a'); 708 | link.href = '#'; 709 | link.setAttribute('class', 'dropdown-item'); 710 | const label = work.firstLine; 711 | link.appendChild(document.createTextNode(label)); 712 | link.appendChild(document.createElement('br')); 713 | const date = document.createElement('span'); 714 | date.setAttribute('class', 'date'); 715 | date.appendChild(document.createTextNode('(' + this.toLocaleString(new Date(work.created)) + ')')); 716 | link.appendChild(date); 717 | link.addEventListener('click', (x => { 718 | return () => { 719 | this.hideNavbar(); 720 | this.load(x); 721 | }; 722 | })(work)); 723 | history.appendChild(link); 724 | }); 725 | if (works.length > 0) { 726 | this.appendDividerTo(history); 727 | const lastA = document.createElement('a'); 728 | lastA.href = '#'; 729 | lastA.setAttribute('class', 'dropdown-item'); 730 | lastA.innerText = 'Last'; 731 | lastA.addEventListener('click', () => { 732 | this.hideNavbar(); 733 | this.onBtnLastClicked(); 734 | }); 735 | history.appendChild(lastA); 736 | } 737 | if (this.getWorkStorage().canProvideTopSites()) { 738 | const topSitesA = document.createElement('a'); 739 | topSitesA.href = '#'; 740 | topSitesA.setAttribute('class', 'dropdown-item'); 741 | topSitesA.innerText = 'Top Sites'; 742 | topSitesA.addEventListener('click', () => { 743 | this.hideNavbar(); 744 | this.onBtnTopSitesClicked(); 745 | }); 746 | history.appendChild(topSitesA); 747 | } 748 | this.appendDividerTo(history); 749 | const howToUseA = document.createElement('a'); 750 | howToUseA.href = '#'; 751 | howToUseA.setAttribute('class', 'dropdown-item'); 752 | howToUseA.innerHTML = 'How to Use'; 753 | howToUseA.addEventListener('click', () => { 754 | this.hideNavbar(); 755 | this.onBtnHowToUseClicked(); 756 | }); 757 | history.appendChild(howToUseA); 758 | 759 | this.showStatusMessage('Loaded mindmaps.'); 760 | 761 | if (callback) { 762 | callback(works); 763 | } 764 | }); 765 | } 766 | 767 | load(work) { 768 | this.loading = true; 769 | let cursorPosition = this.editor.getCursorPosition(); 770 | this.currentWork = work; 771 | this.editor.setValue(this.currentWork.content); 772 | this.editor.clearSelection(); 773 | this.drawMindmap(); 774 | this.editor.focus(); 775 | this.editor.gotoLine(cursorPosition.row + 1, cursorPosition.column, false); 776 | localStorage.lastLoaded = work.created; 777 | this.loading = false; 778 | } 779 | 780 | jumpCaretTo(position) { 781 | let source = this.editor.getValue(); 782 | let lines = source.split(/\n/); 783 | let charCount = 0; 784 | let row = 0; 785 | for (let i = 0; i < lines.length; i += 1) { 786 | let eol = lines[i].length + 1; // '\n' 787 | if (position < charCount + eol) { 788 | row = i + 1; 789 | break; 790 | } 791 | charCount += eol; 792 | } 793 | this.editor.gotoLine(row, position - charCount, false); 794 | this.editor.focus(); 795 | } 796 | 797 | // Status bar 798 | 799 | showStatusMessage(message) { 800 | const statusBar = document.querySelector('#statusMessage'); 801 | statusBar.innerHTML = message; 802 | } 803 | 804 | // Utilities 805 | 806 | toLocaleString(date) { 807 | return [ 808 | date.getFullYear(), 809 | date.getMonth() + 1, 810 | date.getDate() 811 | ].join('/') + ' ' + date.toLocaleTimeString(); 812 | } 813 | 814 | hideNavbar() { 815 | $('.navbar-collapse').collapse('hide'); 816 | } 817 | 818 | isFilterStrikeThroughText() { 819 | return JSON.parse(localStorage.filterStrikeThrough || 'false'); 820 | } 821 | 822 | setConfigrationToUI() { 823 | const filterStrikeThrough = this.isFilterStrikeThroughText(); 824 | document.querySelector('#btnFilterStrikeThroughTextModeOn').checked = !filterStrikeThrough; 825 | document.querySelector('#btnFilterStrikeThroughTextModeOff').checked = filterStrikeThrough; 826 | const lineColorMode = JSON.parse(localStorage.lineColorMode || 'false'); 827 | document.querySelector('#btnLineColorModeOn').checked = lineColorMode; 828 | document.querySelector('#btnLineColorModeOff').checked = !lineColorMode; 829 | const fontSize = Number(localStorage.fontSize || '14'); // Defined at mindmap.js 830 | document.querySelector(`#btnFontSize${fontSize}`).checked = true; 831 | const wingMode = localStorage.wingMode || 'both'; 832 | if (wingMode === 'both') { 833 | document.querySelector('#btnWingModeBoth').checked = true; 834 | } else if (wingMode === 'left') { 835 | document.querySelector('#btnWingModeLeftOnly').checked = true; 836 | } else { 837 | document.querySelector('#btnWingModeRightOnly').checked = true; 838 | } 839 | const showLastMindmapMode = JSON.parse(localStorage.showLastMindmapMode || 'false'); 840 | document.querySelector('#btnShowLastMindmapModeOn').checked = showLastMindmapMode; 841 | document.querySelector('#btnShowLastMindmapModeOff').checked = !showLastMindmapMode; 842 | } 843 | 844 | } 845 | 846 | window.addEventListener('load', () => { 847 | new Newtab(); 848 | }); 849 | -------------------------------------------------------------------------------- /app/scripts/node.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import Token from './token.js'; 4 | 5 | export default class Node { 6 | 7 | constructor(source, position) { 8 | this.tokens = []; 9 | this.children = []; 10 | this.parent = null; 11 | this.id = null; 12 | this.source = source ? source.trim() : ''; 13 | this._parseText(this.source); 14 | this.position = position; 15 | } 16 | 17 | _parseText(source) { 18 | const tempTokens = this._parseUrl(source); 19 | tempTokens.forEach(token => { 20 | if (token.hasUrl()) { 21 | this.tokens.push(token); 22 | } else{ 23 | this._parseStrikeThrough(token).forEach(x => { 24 | this._parseBold(x).forEach(y => { 25 | this.tokens.push(y); 26 | }); 27 | }); 28 | } 29 | }); 30 | } 31 | 32 | _parseStrikeThrough(token) { 33 | const source = token.text; 34 | const re = /~~(.+?)~~/g; 35 | let pos = 0; 36 | let rs = re.exec(source); 37 | const tempTokens = []; 38 | while (rs) { 39 | if (pos < rs.index) { 40 | tempTokens.push(new Token(source.substring(pos, rs.index), null, token.isBold(), false)); 41 | } 42 | let text = rs[1]; 43 | tempTokens.push(new Token(text, null, token.isBold(), true)); 44 | pos = rs.index + rs[0].length; 45 | rs = re.exec(source); 46 | } 47 | if (pos < source.length) { 48 | tempTokens.push(new Token(source.substring(pos), null, token.isBold(), false)); 49 | } 50 | return tempTokens; 51 | } 52 | 53 | _parseBold(token) { 54 | const source = token.text; 55 | const re = /\*\*(.+?)\*\*/g; 56 | let pos = 0; 57 | let rs = re.exec(source); 58 | const tempTokens = []; 59 | while (rs) { 60 | if (pos < rs.index) { 61 | tempTokens.push(new Token(source.substring(pos, rs.index), null, false, token.isStrikeThrough())); 62 | } 63 | let text = rs[1]; 64 | tempTokens.push(new Token(text, null, true, token.isStrikeThrough())); 65 | pos = rs.index + rs[0].length; 66 | rs = re.exec(source); 67 | } 68 | if (pos < source.length) { 69 | tempTokens.push(new Token(source.substring(pos), null, false, token.isStrikeThrough())); 70 | } 71 | return tempTokens; 72 | } 73 | 74 | _parseUrl(source) { 75 | const re = /\[(.+?)]\((.+?)\)/g; 76 | let pos = 0; 77 | let rs = re.exec(source); 78 | const tempTokens = []; 79 | while (rs) { 80 | if (pos < rs.index) { 81 | tempTokens.push(new Token(source.substring(pos, rs.index), null, false, false)); 82 | } 83 | let text = rs[1]; 84 | let url = rs[2]; 85 | tempTokens.push(new Token(text, url, false, false)); 86 | pos = rs.index + rs[0].length; 87 | rs = re.exec(source); 88 | } 89 | if (pos < source.length) { 90 | tempTokens.push(new Token(source.substring(pos), null, false, false)); 91 | } 92 | return tempTokens; 93 | } 94 | 95 | get text() { 96 | return this.tokens.map(token => { return token.text; }).join(''); 97 | } 98 | 99 | get html() { 100 | return this.tokens.map(token => { return token.toHtml(); }).join(''); 101 | } 102 | 103 | static root(text) { 104 | return new Node(text, 0); 105 | } 106 | 107 | add(text, position, callback) { 108 | let child = new Node(text, position); 109 | child.setParent(this); 110 | this.children.push(child); 111 | if (callback) { 112 | callback(child); 113 | } 114 | return this; 115 | } 116 | 117 | setParent(parent) { 118 | this.parent = parent; 119 | } 120 | 121 | isRoot() { 122 | return this.parent == null; 123 | } 124 | 125 | isLeaf() { 126 | return this.children.length === 0; 127 | } 128 | 129 | static visit(node, callback) { 130 | callback(node); 131 | node.children.forEach(child => { 132 | Node.visit(child, callback); 133 | }); 134 | } 135 | 136 | } 137 | -------------------------------------------------------------------------------- /app/scripts/parser.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import Node from './node.js'; 4 | 5 | export default class Parser { 6 | 7 | // Public functions 8 | 9 | parse(source, filterStrikeThroughText) { 10 | const lines = source.split(/\n/); 11 | let root = null; 12 | let prevNode = null; 13 | let prevLevel = -1; 14 | let position = 0; 15 | for (let i = 0; i < lines.length; i += 1) { 16 | const trimedLine = this._trim(lines[i]); 17 | if (trimedLine && this._canShowText(filterStrikeThroughText, trimedLine)) { 18 | const level = this._getIndentLevel(lines[i]); 19 | if (i === 0) { 20 | if (level === 0) { 21 | root = Node.root(lines[i]); 22 | prevNode = root; 23 | prevLevel = 0; 24 | } else { 25 | console.log('Invalid first line.'); 26 | return null; 27 | } 28 | } else { 29 | if (prevLevel === level) { 30 | const parentNode = prevNode.parent; 31 | if (parentNode) { 32 | let node = null; 33 | parentNode.add(lines[i], position + level, x => { 34 | node = x; 35 | }); 36 | prevLevel = level; 37 | prevNode = node; 38 | } else { 39 | console.log('Parent is null.'); 40 | return null; 41 | } 42 | } else if (level < prevLevel) { 43 | let parentNode = prevNode.parent; 44 | for (let j = 0; j < prevLevel - level; j += 1) { 45 | parentNode = parentNode.parent; 46 | } 47 | if (parentNode) { 48 | let node = null; 49 | parentNode.add(lines[i], position + level, x => { 50 | node = x; 51 | }); 52 | prevLevel = level; 53 | prevNode = node; 54 | } else { 55 | console.log('Parent is null.'); 56 | return null; 57 | } 58 | } else if (prevLevel === level - 1) { 59 | let node = null; 60 | prevNode.add(lines[i], position + level, x => { 61 | node = x; 62 | }); 63 | prevLevel = level; 64 | prevNode = node; 65 | } else { 66 | console.log('Invalid indent.', i, lines[i]); 67 | // return null; 68 | } 69 | } 70 | } 71 | position += lines[i].length + 1; 72 | } 73 | return root; 74 | } 75 | 76 | // Private functions 77 | 78 | _canShowText(filterStrikeThroughText, line) { 79 | if (filterStrikeThroughText) { 80 | return !/^~~[^~]+~~$/.test(line); 81 | } else { 82 | return true; 83 | } 84 | } 85 | 86 | _getIndentLevel(text) { 87 | let level = 0; 88 | let inSpaces = false; 89 | let spaceCount = 0; 90 | for (let i = 0; i < text.length; i += 1) { 91 | if (text.charAt(i) === '\t' && !inSpaces) { 92 | level += 1; 93 | } else if (text.charAt(i) === ' ') { 94 | inSpaces = true; 95 | spaceCount += 1; 96 | if (spaceCount === 4) { 97 | level += 1; 98 | inSpaces = false; 99 | spaceCount = 0; 100 | } 101 | } else { 102 | break; 103 | } 104 | } 105 | return level; 106 | } 107 | 108 | _trim(text) { 109 | let result = ''; 110 | for (let i = 0; i < text.length; i += 1) { 111 | if (text.charAt(i) !== '\t' && text.charAt(i) !== ' ') { 112 | result += text.charAt(i); 113 | } 114 | } 115 | return result; 116 | } 117 | 118 | } 119 | -------------------------------------------------------------------------------- /app/scripts/service_worker.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | self.addEventListener('fetch', () => {}); 4 | -------------------------------------------------------------------------------- /app/scripts/service_worker_loader.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | window.addEventListener('load', () => { 4 | if ('serviceWorker' in navigator) { 5 | navigator.serviceWorker.register('service_worker.js') 6 | .then(() => { 7 | console.log('serviceWorker registered'); 8 | }).catch(error => { 9 | console.warn('serviceWorker error', error); 10 | }); 11 | } 12 | }); 13 | -------------------------------------------------------------------------------- /app/scripts/token.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | export default class Token { 4 | 5 | constructor(text, url, bold, strikeThrough) { 6 | this.text = text; 7 | this.url = url; 8 | this.bold = bold; 9 | this.strikeThrough = strikeThrough; 10 | } 11 | 12 | hasUrl() { 13 | return this.url && this.url.length > 0; 14 | } 15 | 16 | isBold() { 17 | return this.bold; 18 | } 19 | 20 | isStrikeThrough() { 21 | return this.strikeThrough; 22 | } 23 | 24 | toHtml() { 25 | if (this.hasUrl()) { 26 | return '' + this.escapeHTML(this.text) + ''; 27 | } else if (this.isBold()) { 28 | return '' + this.escapeHTML(this.text) + ''; 29 | } else if (this.isStrikeThrough()) { 30 | return '' + this.escapeHTML(this.text) + ''; 31 | } else { 32 | return '' + this.escapeHTML(this.text) + ''; 33 | } 34 | } 35 | 36 | escapeHTML(html) { 37 | let e = document.createElement('div'); 38 | e.appendChild(document.createTextNode(html)); 39 | return e.innerHTML; 40 | } 41 | 42 | } 43 | -------------------------------------------------------------------------------- /app/scripts/work.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | export default class Work { 4 | 5 | constructor(created, content, updated) { 6 | this.created = created; 7 | this.content = content; 8 | this.updated = updated; 9 | this.isSave = true; 10 | } 11 | 12 | static newInstance() { 13 | let now = Date.now(); 14 | return new Work(now, '', now); 15 | } 16 | 17 | get firstLine() { 18 | let lines = this.content.split(/\r\n|\r|\n/); 19 | if (lines && lines.length > 0) { 20 | return lines[0]; 21 | } else { 22 | return ''; 23 | } 24 | } 25 | 26 | hasContent() { 27 | return this.content && this.content.trim().length > 0; 28 | } 29 | 30 | } 31 | -------------------------------------------------------------------------------- /app/styles/newtab.css: -------------------------------------------------------------------------------- 1 | html, body { 2 | height: 100%; 3 | margin: 0; 4 | background-color: white; 5 | } 6 | 7 | #source { 8 | width: 100%; 9 | height: 100%; 10 | border: lightgray solid 1px; 11 | line-height: 1.4; 12 | } 13 | 14 | div.main { 15 | position: absolute; 16 | top: 58px; 17 | bottom: 33px; 18 | width: 100%; 19 | } 20 | 21 | div.main div.row { 22 | height: 100%; 23 | } 24 | 25 | div.main div.row>div { 26 | height: 100%; 27 | } 28 | 29 | nav { 30 | max-height: 56px; 31 | } 32 | 33 | div.footer { 34 | position: absolute; 35 | bottom: 5px; 36 | width: 100%; 37 | height: 25px; 38 | } 39 | 40 | div.statusBar { 41 | position: relative; 42 | right: 100px; 43 | left: 0px; 44 | width: 100%; 45 | height: 100%; 46 | border: lightgray solid 1px; 47 | font-size: 12px; 48 | padding-left: 10px; 49 | padding-right: 10px; 50 | padding-top: 3px; 51 | } 52 | 53 | div.footerButtons img { 54 | cursor: pointer; 55 | } 56 | 57 | #statusMessage { 58 | left: 10px; 59 | right: 100px; 60 | position: absolute; 61 | text-overflow: ellipsis; 62 | overflow: hidden; 63 | white-space: nowrap; 64 | } 65 | 66 | div.canvas-container { 67 | position: relative; 68 | height: 100%; 69 | overflow: auto; 70 | border: lightgray solid 1px; 71 | } 72 | 73 | canvas { 74 | font-size: 14px; 75 | font-family: sans-serif; 76 | display: block; 77 | position: absolute; 78 | /* 79 | top: 0; 80 | bottom: 0; 81 | */ 82 | left: 0; 83 | right: 0; 84 | margin: auto; 85 | } 86 | 87 | nav { 88 | margin-left: 15px; 89 | margin-right: 15px; 90 | z-index: 10; 91 | } 92 | 93 | #copyBuffer { 94 | position: absolute; 95 | top: -10px; 96 | left: -10px; 97 | width: 0; 98 | height: 0; 99 | margin: 0; 100 | padding: 0; 101 | } 102 | 103 | #loginDialog .modal-footer a { 104 | margin-right: 10px; 105 | } 106 | 107 | .errorMessage { 108 | color: red; 109 | } 110 | 111 | #history { 112 | max-height: 500px; 113 | min-width: 300px; 114 | overflow-y: auto; 115 | font-size: 0.95rem; 116 | } 117 | 118 | #history a { 119 | white-space: inherit; 120 | } 121 | 122 | #history a .date { 123 | font-size: 0.7rem; 124 | color: gray; 125 | } 126 | 127 | #configuration { 128 | min-width: 350px; 129 | } 130 | 131 | #calendar h2 { 132 | font-size: 1.15rem; 133 | } 134 | 135 | #calendar button { 136 | font-size: 0.8rem; 137 | } 138 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | const gulp = require('gulp'); 2 | const webpackStream = require('webpack-stream'); 3 | const webpack = require('webpack'); 4 | const rm = require('rimraf'); 5 | const zip = require('gulp-zip'); 6 | const eslint = require('gulp-eslint'); 7 | 8 | const webpackConfig = require('./webpack.config'); 9 | 10 | gulp.task('clean', done => { 11 | rm('./dist', done); 12 | }); 13 | 14 | gulp.task('lint', () => { 15 | return gulp.src([ 16 | './app/scripts/*.js' 17 | ]).pipe(eslint({ 18 | useEslintrc: true 19 | })).pipe(eslint.format()).pipe(eslint.failAfterError()); 20 | }); 21 | 22 | gulp.task('copy-src-files', () => { 23 | return gulp.src([ 24 | './app/*.*', 25 | './app/_locales/**', 26 | './app/scripts/background.js', 27 | './app/libs/ace/*.js', 28 | './app/styles/*.css', 29 | './app/images/*.ico', 30 | './app/images/*.png', 31 | './app/images/*.gif' 32 | ], { 33 | base: 'app' 34 | }).pipe(gulp.dest('./dist')); 35 | }); 36 | 37 | gulp.task('copy-dependent-files', () => { 38 | return gulp.src([ 39 | './node_modules/jquery/dist/jquery.min.js', 40 | './node_modules/jcanvas/dist/jcanvas.js', 41 | './node_modules/bootstrap/dist/js/bootstrap.min.js', 42 | './node_modules/bootstrap/dist/css/bootstrap.min.css', 43 | './node_modules/responsive-toolkit/dist/bootstrap-toolkit.min.js', 44 | './node_modules/fullcalendar/dist/fullcalendar.min.css', 45 | './node_modules/moment/min/moment.min.js', 46 | './node_modules/fullcalendar/dist/fullcalendar.min.js' 47 | ], { 48 | base: 'node_modules' 49 | }).pipe(gulp.dest('./dist/node_modules')); 50 | }); 51 | 52 | gulp.task('copy-files', gulp.parallel('copy-src-files', 'copy-dependent-files')); 53 | 54 | gulp.task('package', () => { 55 | const manifest = require('./dist/manifest.json'); 56 | const version = manifest.version; 57 | return gulp.src('./dist/**/*').pipe(zip(`mindmap-tab-${version}.zip`)).pipe(gulp.dest('./package')); 58 | }); 59 | 60 | gulp.task('webpack', () => { 61 | return webpackStream(webpackConfig,webpack) 62 | .pipe(gulp.dest('dist/scripts')); 63 | }); 64 | 65 | gulp.task('service-worker', () => { 66 | return gulp.src('app/scripts/service_worker*.js') 67 | .pipe(gulp.dest('dist')); 68 | }); 69 | 70 | gulp.task('default', gulp.series('clean', 'lint', 'webpack', 'service-worker', 'copy-files', 'package')); 71 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mindmap-tab", 3 | "private": true, 4 | "engines": { 5 | "node": ">=0.8.0" 6 | }, 7 | "devDependencies": { 8 | "@babel/core": "^7.6.4", 9 | "babel-loader": "^8.0.6", 10 | "babel-preset-es2015": "^6.24.1", 11 | "chai": "^4.2.0", 12 | "gulp": "^4.0.2", 13 | "gulp-eslint": "^6.0.0", 14 | "gulp-zip": "^5.0.1", 15 | "mocha": "^6.2.2", 16 | "rimraf": "^3.0.0", 17 | "webpack": "^4.41.2", 18 | "webpack-cli": "^3.3.9", 19 | "webpack-stream": "^5.2.1" 20 | }, 21 | "dependencies": { 22 | "bootstrap": "^4.3.1", 23 | "commonjs": "^0.0.1", 24 | "fullcalendar": "^3.10.1", 25 | "jcanvas": "^21.0.1", 26 | "jquery": "^3.4.1", 27 | "moment": "^2.24.0", 28 | "responsive-toolkit": "^2.6.3" 29 | }, 30 | "scripts": { 31 | "build": "gulp" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /test/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Mocha Spec Runner 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | 18 | 19 | 20 | 21 | 22 | 23 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /test/spec/test.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | 'use strict'; 3 | 4 | describe('Give it some context', function () { 5 | describe('maybe a bit more context here', function () { 6 | it('should run here few assertions', function () { 7 | 8 | }); 9 | }); 10 | }); 11 | })(); 12 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | module.exports = { 4 | mode: 'development', 5 | entry: path.join(__dirname, 'app/scripts/newtab.js'), 6 | output: { 7 | path: path.join(__dirname, '/dist'), 8 | filename: 'bundle.js' 9 | }, 10 | module: { 11 | rules: [ 12 | { 13 | test: /\.js$/, 14 | exclude: /node_moduls/, 15 | use: [ 16 | { 17 | loader: 'babel-loader' 18 | } 19 | ] 20 | } 21 | ] 22 | }, 23 | devtool: 'source-map', 24 | resolve: { 25 | extensions: ['.js'] 26 | } 27 | }; 28 | --------------------------------------------------------------------------------