├── .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 |
28 | MindMap Tab
29 |
30 |
31 |
32 |
33 |
177 |
178 |
179 |
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 = '\n';
481 | let traverse = (node, currentLevel) => {
482 | for (let i = 0; i < currentLevel; i += 1) {
483 | text += ' ';
484 | }
485 | text += '' + node.html;
486 | if (!node.isLeaf()) {
487 | currentLevel += 1;
488 | text += '\n';
489 | for (let i = 0; i < currentLevel; i += 1) {
490 | text += ' ';
491 | }
492 | text += '\n';
493 | node.children.forEach(child => {
494 | traverse(child, currentLevel + 1);
495 | });
496 | for (let i = 0; i < currentLevel; i += 1) {
497 | text += ' ';
498 | }
499 | text += ' \n';
500 | currentLevel -= 1;
501 | for (let i = 0; i < currentLevel; i += 1) {
502 | text += ' ';
503 | }
504 | }
505 | text += ' \n';
506 | };
507 | traverse(root, 1);
508 | 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 |
--------------------------------------------------------------------------------