├── .gitignore
├── .npmignore
├── LICENSE
├── README.md
├── index.html
├── package-lock.json
├── package.json
└── src
├── commands.js
├── consts.js
├── index.js
├── locale
└── en.js
├── manager
├── index.js
├── pages.js
├── settings.js
└── templates.js
├── storage
├── firestore.js
├── index.js
├── indexeddb.js
└── remote.js
├── styles.scss
└── utils
├── objsize.js
├── sort.js
├── timeago.js
└── ui.js
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | private/
3 | /locale
4 | node_modules/
5 | *.log
6 | _index.html
7 | dist/
8 | stats.json
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | private/
3 | /locale
4 | node_modules/
5 | *.log
6 | _index.html
7 | stats.json
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2021-current Grapesjs Template Manager
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
6 |
7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
8 |
9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Grapesjs Project Manager
2 |
3 | > Requires GrapesJS v0.19.4 or higher.
4 |
5 | Project, template and page manager for grapesjs. This version makes use of the [`PageManager`](https://github.com/artf/grapesjs/pull/3411) and has different plugin and package name, the previous version which doesn't make use of the `PageManager` can be found [here](https://github.com/Ju99ernaut/grapesjs-template-manager/tree/template-manager).
6 |
7 | | Project | Project settings |
8 | |---------|------------------|
9 | |  |  |
10 |
11 | | Pages | Page settings |
12 | |-------|---------------|
13 | |  |  |
14 |
15 | ### HTML
16 | ```html
17 |
18 |
19 |
20 |
21 |
22 |
23 | ```
24 |
25 | ### JS
26 | ```js
27 | const editor = grapesjs.init({
28 | container: '#gjs',
29 | height: '100%',
30 | fromElement: true,
31 | pageManager: true, // This should be set to true
32 | storageManager: {
33 | type: 'indexeddb',
34 | // ...
35 | },
36 | plugins: ['grapesjs-project-manager'],
37 | });
38 |
39 | // Running commands from panels
40 | const pn = editor.Panels;
41 | pn.addButton('options', {
42 | id: 'open-templates',
43 | className: 'fa fa-folder-o',
44 | attributes: {
45 | title: 'Open projects and templates'
46 | },
47 | command: 'open-templates', //Open modal
48 | });
49 | pn.addButton('views', {
50 | id: 'open-pages',
51 | className: 'fa fa-file-o',
52 | attributes: {
53 | title: 'Take Screenshot'
54 | },
55 | command: 'open-pages',
56 | togglable: false
57 | });
58 | ```
59 |
60 | ### CSS
61 | ```css
62 | body, html {
63 | margin: 0;
64 | height: 100%;
65 | }
66 | ```
67 |
68 |
69 | ## Summary
70 |
71 | * Plugin name: `grapesjs-project-manager`
72 | * Commands
73 | * `open-templates`
74 | * `open-pages`
75 | * `open-settings`
76 | * `get-uuidv4`
77 | * `take-screenshot`
78 | * `save-as-template`
79 | * `delete-template`
80 | * Storages
81 | * `indexeddb`
82 | * `firestore`
83 | * `rest-api`
84 |
85 | ## Options
86 |
87 | | Option | Description | Default |
88 | |-|-|-
89 | | `dbName` | Database name | `gjs` |
90 | | `objectStoreName` | Collection name | `templates` |
91 | | `loadFirst` | Load first template in storage | `true` |
92 | | `customLoad` | Use custom onload function(skips default onload steps), `(ed, cs) => ...` | `false` |
93 | | `components` | Default components since `fromElement` is not supported | `undefined` |
94 | | `style` | Default style since `fromElement` is not supported | `undefined` |
95 | | `indexeddbVersion` | IndexedDB schema version | `5` |
96 | | `onDelete` | On successful template deletion | `Function(Check source)` |
97 | | `onDeleteAsync` | Handle promise from storage delete | `Function(Check source)` |
98 | | `onUpdateAsync` | Handle promise from storage update | `Function(Check source)` |
99 | | `onScreenshotAsync` | Handle promise from screenshot | `Function(Check source)` |
100 | | `onScreenShotError` | On error capturing screenshot | `Function(Check source)` |
101 | | `onThumbnail` | Handle thumbnail data | `Function(Check source)` |
102 | | `quality` | Generated screenshot quality | `.01` |
103 | | `mdlTitle` | Modal title | `Project Manager` |
104 | | `apiKey` | `Firebase` API key | ` ` |
105 | | `authDomain` | `Firebase` Auth domain | ` ` |
106 | | `projectId` | `Cloud Firestore` project ID | ` ` |
107 | | `firebaseConfig` | Extra firebase app credentials | `{}` |
108 | | `enableOffline` | Enable `Firestore` support for offline data persistence | `true` |
109 | | `settings` | `Firestore` database settings | `{ timestampsInSnapshots: true }` |
110 | | `uuidInPath` | Add uuid as path parameter on store for `rest-api`(useful for validation) | `true` |
111 | | `size` | Display estimated project sizes | `true` |
112 | | `currentPageOpen` | Send feedback when open is clicked on current page | `check source` |
113 | | `ì18n` | I18n object containing language [more info](https://grapesjs.com/docs/modules/I18n.html#configuration) | `{}` |
114 |
115 | * Setting `loadFirst` to `false` prevents overwritting the contents of the editor with the contents of the first template in storage.
116 | * Only use options for `Firebase` when using `Cloud Firestore` storage.
117 | * `dbName` and `indexeddbVersion` only apply to `indexddb` storage.
118 | * `objectStoreName` acts as collection name for both `firestore` and ` indexeddb`.
119 | * When `uuidInPath` is set to `false` the store request will be `http://endpoint/store/` instead of `http://endpoint/store/{uuid}`
120 |
121 | ## Local/IndexedDB
122 |
123 | ```js
124 | window.editor = grapesjs.init({
125 | container: '#gjs',
126 | // ...
127 | pageManager: true,
128 | storageManager: {
129 | type: 'indexeddb'
130 | },
131 | plugins: ['grapesjs-project-manager'],
132 | pluginsOpts: {
133 | 'grapesjs-project-manager': { /* Options */ }
134 | }
135 | });
136 | ```
137 |
138 | ## Firestore
139 |
140 | > Tested on firebase v8+. Firebase v9+ not yet supported.
141 |
142 | Configure firestore access rules for your app.
143 | Add libraries to `head` of document:
144 |
145 | ```html
146 |
147 |
148 |
150 |
151 | ```
152 |
153 | Add credentials:
154 |
155 | ```js
156 | window.editor = grapesjs.init({
157 | container: '#gjs',
158 | // ...
159 | pageManager: true,
160 | storageManager: {
161 | type: 'firestore'
162 | },
163 | plugins: ['grapesjs-project-manager'],
164 | pluginsOpts: {
165 | 'grapesjs-project-manager': {
166 | // Firebase API key
167 | apiKey: 'FIREBASE_API_KEY',
168 | // Firebase Auth domain
169 | authDomain: 'app-id-00a00.firebaseapp.com',
170 | // Cloud Firestore project ID
171 | projectId: 'app-id-00a00',
172 | }
173 | }
174 | });
175 | ```
176 |
177 | ## Remote/REST-API
178 |
179 | Example backend https://github.com/Ju99ernaut/gjs-api
180 |
181 | ```js
182 | window.editor = grapesjs.init({
183 | container: '#gjs',
184 | // ...
185 | pageManager: true,
186 | storageManager: {
187 | type: 'rest-api',
188 | // the URIs below can be the same depending on your API design
189 | options: {
190 | remote: {
191 | urlStore: 'https://endpoint/store/',// POST
192 | urlLoad: 'https://endpoint/load/',// GET
193 | urlDelete: 'https://endpoint/delete/',// DELETE
194 | // ...
195 | }
196 | }
197 | },
198 | plugins: ['grapesjs-project-manager'],
199 | pluginsOpts: {
200 | 'grapesjs-project-manager': { /* options */ }
201 | }
202 | });
203 | ```
204 |
205 | The backend schema can be something like:
206 |
207 | `GET` `https://api/templates/` load all templates
208 |
209 | Returns
210 | ```json
211 | [
212 | {
213 | "id": "UUIDv4",
214 | "name": "Page name",
215 | "template": false,
216 | "thumbnail": "",
217 | "description": "No description",
218 | "assets": "[]",
219 | "pages": "[]",
220 | "styles": "[]",
221 | "updated_at": ""
222 | }
223 | ]
224 | ```
225 |
226 | `POST` `https://api/templates/{idx: UUIDv4}` store or update template
227 |
228 | Expects
229 | ```json
230 | {
231 | "id": "UUIDv4",
232 | "name": "Page name",
233 | "template": false,
234 | "thumbnail": "",
235 | "description": "No description",
236 | "assets": "[]",
237 | "pages": "[]",
238 | "styles": "[]",
239 | "updated_at": ""
240 | }
241 | ```
242 |
243 | `GET` `https://api/templates/{idx: UUIDv4}` load template
244 |
245 | Returns
246 | ```json
247 | {
248 | "id": "UUIDv4",
249 | "name": "Page name",
250 | "template": false,
251 | "thumbnail": "",
252 | "description": "No description",
253 | "assets": "[]",
254 | "pages": "[]",
255 | "styles": "[]",
256 | "updated_at": ""
257 | }
258 | ```
259 |
260 | `DELETE` `https://api/templates/{idx: UUIDv4}` delete template
261 |
262 | Which would have the following setup:
263 | ```js
264 | window.editor = grapesjs.init({
265 | container: '#gjs',
266 | // ...
267 | storageManager: {
268 | type: 'rest-api',
269 | // the URIs below can be the same depending on your API design
270 | options:{
271 | remote:{
272 | urlStore: 'https://api/templates/',// POST
273 | urlLoad: 'https://api/templates/',// GET
274 | urlDelete: 'https://api/templates/',// DELETE
275 | }
276 | }
277 | },
278 | plugins: ['grapesjs-template-manager'],
279 | pluginsOpts: {
280 | 'grapesjs-template-manager': { /* options */ }
281 | }
282 | });
283 | ```
284 |
285 | All the fields are generated from the editor so you just need to setup your API to receive and return data in that format. I'd recommend you check the network tab so you get a more accurate format for the payloads.
286 |
287 | ## Download
288 |
289 | * CDN
290 | * `https://unpkg.com/grapesjs-project-manager`
291 | * NPM
292 | * `npm i grapesjs-project-manager`
293 | * GIT
294 | * `git clone https://github.com/Ju99ernaut/grapesjs-template-manager.git`
295 |
296 |
297 |
298 | ## Usage
299 |
300 | Directly in the browser
301 | ```html
302 |
303 |
304 |
305 |
306 |
307 |
308 |
309 |
324 | ```
325 |
326 | Modern javascript
327 | ```js
328 | import grapesjs from 'grapesjs';
329 | import plugin from 'grapesjs-project-manager';
330 | import 'grapesjs/dist/css/grapes.min.css';
331 | import 'grapesjs-project-manager/dist/grapesjs-project-manager.min.css';
332 |
333 | const editor = grapesjs.init({
334 | container : '#gjs',
335 | // ...
336 | pageManager: true,
337 | storageManager: {
338 | type: 'indexeddb',
339 | // ...
340 | },
341 | plugins: [plugin],
342 | pluginsOpts: {
343 | [plugin]: { /* options */ }
344 | }
345 | // or
346 | plugins: [
347 | editor => plugin(editor, { /* options */ }),
348 | ],
349 | });
350 | ```
351 |
352 | ## Development
353 |
354 | Clone the repository
355 |
356 | ```sh
357 | $ git clone https://github.com/Ju99ernaut/grapesjs-template-manager.git
358 | $ cd grapesjs-template-manager
359 | ```
360 |
361 | Install dependencies
362 |
363 | ```sh
364 | $ npm i
365 | ```
366 |
367 | Build css or watch scss
368 |
369 | ```sh
370 | $ npm run build:css
371 | ```
372 |
373 | `OR`
374 |
375 | ```
376 | $ npm run watch:scss
377 | ```
378 |
379 | Start the dev server
380 |
381 | ```sh
382 | $ npm start
383 | ```
384 |
385 | Build the source
386 |
387 | ```sh
388 | $ npm run build
389 | ```
390 |
391 |
392 |
393 | ## License
394 |
395 | MIT
396 |
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Grapesjs Project Manager
7 |
8 |
9 |
10 |
17 |
18 |
19 |
20 |
21 |
22 |
23 | This is a demo content from _index.html. You can use this template file for
24 | development purpose. It won't be stored in your git repository
25 |
26 |
27 |
28 |
29 |
64 |
65 |
66 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "grapesjs-project-manager",
3 | "version": "2.0.6",
4 | "description": "Grapesjs Project Manager",
5 | "main": "dist/grapesjs-project-manager.min.js",
6 | "repository": {
7 | "type": "git",
8 | "url": "https://github.com/Ju99ernaut/grapesjs-template-manager.git"
9 | },
10 | "scripts": {
11 | "start": "grapesjs-cli serve",
12 | "build": "grapesjs-cli build",
13 | "build:css": "sass src/styles.scss dist/grapesjs-project-manager.min.css --style compressed",
14 | "watch:scss": "sass --watch src/styles.scss dist/grapesjs-project-manager.min.css --style compressed",
15 | "bump": "npm version patch -m 'Bump v%s'"
16 | },
17 | "keywords": [
18 | "grapesjs",
19 | "plugin"
20 | ],
21 | "devDependencies": {
22 | "grapesjs-cli": "^1.0.14",
23 | "sass": "^1.32.8"
24 | },
25 | "author": "Brendon Ngirazi",
26 | "license": "MIT",
27 | "dependencies": {
28 | "dom-to-image": "^2.6.0"
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/src/commands.js:
--------------------------------------------------------------------------------
1 | import domtoimage from 'dom-to-image';
2 |
3 | export default (editor, opts = {}) => {
4 | const cm = editor.Commands;
5 | const cs = editor.Storage.getCurrentStorage();
6 | const mdl = editor.Modal;
7 | const pfx = editor.getConfig('stylePrefix');
8 | const mdlClass = `${pfx}mdl-dialog-tml`;
9 | const mdlClassMd = `${pfx}mdl-dialog-md`;
10 |
11 | editor.domtoimage = domtoimage;
12 |
13 | cm.add('open-templates', {
14 | run(editor, sender) {
15 | const mdlDialog = document.querySelector(`.${pfx}mdl-dialog`);
16 | mdlDialog.classList.add(mdlClass);
17 | sender?.set && sender.set('active');
18 | mdl.setTitle(opts.mdlTitle);
19 | mdl.setContent(editor.TemplateManager.render());
20 | mdl.open();
21 | mdl.getModel().once('change:open', () => {
22 | mdlDialog.classList.remove(mdlClass);
23 | });
24 | }
25 | });
26 |
27 | cm.add('open-settings', {
28 | run(editor, sender) {
29 | const mdlDialog = document.querySelector(`.${pfx}mdl-dialog`);
30 | mdlDialog.classList.add(mdlClassMd);
31 | sender?.set && sender.set('active');
32 | mdl.setTitle(opts.mdlTitle);
33 | mdl.setContent(editor.SettingsApp.render());
34 | mdl.open();
35 | mdl.getModel().once('change:open', () => {
36 | mdlDialog.classList.remove(mdlClassMd);
37 | });
38 | }
39 | });
40 |
41 | cm.add('open-pages', {
42 | run(editor) {
43 | editor.PagesApp.showPanel();
44 | },
45 | stop(editor) {
46 | editor.PagesApp.hidePanel();
47 | }
48 | })
49 |
50 | //some magic from gist.github.com/jed/982883
51 | const uuidv4 = () => ([1e7] + -1e3 + -4e3 + -8e3 + -1e11).replace(/[018]/g, c =>
52 | (c ^ crypto.getRandomValues(new Uint8Array(1))[0] & 15 >> c / 4).toString(16)
53 | );
54 |
55 | const getJpeg = async (node, options = {}, clb, clbErr) => {
56 | try {
57 | const dataUrl = await opts.onScreenshotAsync(domtoimage.toJpeg(node, options));
58 | clb && clb(dataUrl);
59 | } catch (err) {
60 | clbErr && clbErr(err)
61 | }
62 | };
63 |
64 | cm.add('get-uuidv4', () => {
65 | if (crypto) {
66 | return crypto.randomUUID ? crypto.randomUUID() : uuidv4();
67 | }
68 | });
69 |
70 | cm.add('take-screenshot', (editor, s, options = { clb(d) { return d } }) => {
71 | const el = editor.getWrapper().getEl();
72 | getJpeg(el, {
73 | quality: opts.quality,
74 | height: 1000,
75 | 'cacheBust': true,
76 | style: {
77 | 'background-color': 'white',
78 | ...editor.getWrapper().getStyle()
79 | },
80 | }, options.clb, opts.onScreenshotError);
81 | });
82 |
83 | cm.add('save-as-template', editor => {
84 | cs.setIsTemplate(true);
85 | editor.store();
86 | });
87 |
88 | cm.add('delete-template', async (editor) => {
89 | const res = await cs.delete();
90 | opts.onDelete(res);
91 | });
92 | }
--------------------------------------------------------------------------------
/src/consts.js:
--------------------------------------------------------------------------------
1 | export const storageIDB = 'indexeddb',
2 | storageRemote = 'rest-api',
3 | storageFireStore = 'firestore',
4 | helpers = {
5 | currentName: 'Default',
6 | currentId: 'uuidv4',
7 | currentThumbnail: '',
8 | isTemplate: false,
9 | description: 'No description',
10 |
11 | setId(id) {
12 | this.currentId = id;
13 | },
14 |
15 | setName(name) {
16 | this.currentName = name;
17 | },
18 |
19 | setThumbnail(thumbnail) {
20 | this.currentThumbnail = thumbnail;
21 | },
22 |
23 | setIsTemplate(isTemplate) {
24 | this.isTemplate = !!isTemplate;
25 | },
26 |
27 | setDescription(description) {
28 | this.description = description;
29 | },
30 | };
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import TemplateManager, { PagesApp, SettingsApp } from './manager';
2 | import commands from './commands';
3 | import storage from './storage';
4 | import en from './locale/en';
5 |
6 | export default (editor, opts = {}) => {
7 | const options = {
8 | ...{
9 | // default options
10 | // Allow migration of projects using deprecated storage prefix
11 | legacyPrefix: '',
12 | // Database name
13 | dbName: 'gjs',
14 |
15 | // Collection name
16 | objectStoreName: 'projects',
17 |
18 | // Load first template in storage
19 | loadFirst: true,
20 |
21 | // Custom load
22 | customLoad: false,
23 |
24 | // Add uuid as path parameter to store path for rest-api
25 | uuidInPath: true,
26 |
27 | // Indexeddb version schema
28 | indexeddbVersion: 6,
29 |
30 | // Custom delete function
31 | delete: false,
32 |
33 | // Confirm delete project
34 | confirmDeleteProject() {
35 | return confirm('Are you sure to delete this project')
36 | },
37 |
38 | // Confirm delete page
39 | confirmDeletePage() {
40 | return confirm('Are you sure to delete this page')
41 | },
42 |
43 | // When template or page is deleted
44 | onDelete(res) {
45 | console.log('Deleted:', res)
46 | },
47 |
48 | // Handle promise from delete
49 | onDeleteAsync(del) {
50 | return del;
51 | },
52 |
53 | // Custom update function
54 | update: false,
55 |
56 | // Handle promise from update
57 | onUpdateAsync(up) {
58 | return up;
59 | },
60 |
61 | // Handle promise from screenshot
62 | onScreenshotAsync(shot) {
63 | return shot;
64 | },
65 |
66 | // On screenshot error
67 | onScreenshotError(err) {
68 | console.log(err)
69 | },
70 |
71 | // Handle built-in thumbnail generation
72 | // By default it just sets the url as the base64 encoded image which may be too large to store in a database
73 | // You might want to upload this somewhere
74 | onThumbnail(dataUrl, $input) {
75 | $input.val(dataUrl);
76 | },
77 |
78 | // Quality of screenshot image from 0 to 1, more quality increases the image size
79 | quality: .01,
80 |
81 | // Content for templates modal title
82 | mdlTitle: 'Project Manager',
83 |
84 | // Show when no pages yet pages
85 | nopages: 'No Projects Yet
',
86 |
87 | // Firebase API key
88 | apiKey: '',
89 |
90 | // Firebase Auth domain
91 | authDomain: '',
92 |
93 | // Cloud Firestore project ID
94 | projectId: '',
95 |
96 | // Enable support for offline data persistence
97 | enableOffline: true,
98 |
99 | // Firebase app config
100 | firebaseConfig: {},
101 |
102 | // Database settings (https://firebase.google.com/docs/reference/js/firebase.firestore.Settings)
103 | settings: { timestampsInSnapshots: true },
104 |
105 | // Show estimated project statistics
106 | size: false,
107 |
108 | // Send feedback when open is clicked on current page
109 | currentPageOpen() {
110 | console.log('Current page already open')
111 | },
112 |
113 | i18n: {},
114 | },
115 | ...opts,
116 | };
117 |
118 | editor.I18n.addMessages({
119 | en,
120 | ...options.i18n,
121 | });
122 |
123 | // Init and add dashboard object to editor
124 | editor.TemplateManager = new TemplateManager(editor, options);
125 | editor.PagesApp = new PagesApp(editor, options);
126 | editor.SettingsApp = new SettingsApp(editor, options);
127 |
128 | // Load commands
129 | commands(editor, options);
130 |
131 | // Load storages
132 | storage(editor, options);
133 |
134 | // Load page with index zero
135 | editor.on('load', async () => {
136 | const cs = editor.Storage.getCurrentStorage();
137 | const { customLoad } = options;
138 | customLoad && typeof customLoad === 'function' && customLoad(editor, cs);
139 | if (!customLoad) {
140 | const res = await cs.loadAll();
141 | const firstPage = res[0];
142 | if (firstPage && options.loadFirst) {
143 | cs.setId(firstPage.id);
144 | cs.setName(firstPage.name);
145 | cs.setThumbnail(firstPage.thumbnail);
146 | cs.setIsTemplate(firstPage.template);
147 | await editor.load();
148 | editor.stopCommand('sw-visibility');
149 | editor.runCommand('sw-visibility');
150 | } else {
151 | cs.setId(editor.runCommand('get-uuidv4'));
152 | cs.setName(`Default-${cs.currentId.substr(0, 7)}`);
153 | }
154 | }
155 | });
156 | };
--------------------------------------------------------------------------------
/src/locale/en.js:
--------------------------------------------------------------------------------
1 | export default {
2 | 'grapesjs-project-manager': {
3 | templates: {
4 | all: 'All',
5 | templates: 'Templates',
6 | search: 'Search for sites by name or id',
7 | open: 'Open',
8 | new: 'Enter new page name',
9 | create: 'Create',
10 | help: 'Select a template, enter project name, then click create. If no template is selected a blank project will be created.',
11 | info: 'Site Info',
12 | updated: 'Last Updated',
13 | pages: 'Pages',
14 | created: 'Created At',
15 | size: 'Size',
16 | actions: 'Actions',
17 | titles: {
18 | open: 'Select to open site',
19 | info: 'Click to sort by site name',
20 | updated: 'Click to sort by last update date',
21 | pages: 'Click to sort by number of pages',
22 | created: 'Click to sort by site creation date',
23 | size: 'Click to sort by site size',
24 | actions: 'Click to sort by site name',
25 | delete: 'Delete',
26 | edit: 'Edit',
27 | }
28 | },
29 | pages: {
30 | placeholder: 'page name',
31 | new: 'New Page +',
32 | },
33 | settings: {
34 | save: 'Save',
35 | generate: 'Generate',
36 | help: 'Enter url, or generate thumbnail.',
37 | labels: {
38 | name: 'Name',
39 | thumbnail: 'Thumbnail',
40 | description: 'Description',
41 | template: 'Template'
42 | },
43 | placeholders: {
44 | name: 'Name...',
45 | thumbnail: 'Thumbnail...',
46 | description: 'Description...',
47 | },
48 | },
49 | }
50 | }
--------------------------------------------------------------------------------
/src/manager/index.js:
--------------------------------------------------------------------------------
1 | import TemplateManager from "./templates";
2 | import PagesApp from "./pages";
3 | import SettingsApp from "./settings";
4 |
5 | export default TemplateManager;
6 | export { TemplateManager, PagesApp, SettingsApp };
--------------------------------------------------------------------------------
/src/manager/pages.js:
--------------------------------------------------------------------------------
1 | import UI from '../utils/ui';
2 |
3 | export default class PagesApp extends UI {
4 | constructor(editor, opts = {}) {
5 | super(editor, opts);
6 | this.addPage = this.addPage.bind(this);
7 | this.selectPage = this.selectPage.bind(this);
8 | this.removePage = this.removePage.bind(this);
9 | this.isSelected = this.isSelected.bind(this);
10 | this.handleNameInput = this.handleNameInput.bind(this);
11 | this.openEdit = this.openEdit.bind(this);
12 |
13 | /* Set initial app state */
14 | this.state = {
15 | editablePageId: '',
16 | isShowing: true,
17 | nameText: '',
18 | pages: [],
19 | loading: false
20 | };
21 | }
22 |
23 | get editableId() {
24 | return this.state.editablePageId;
25 | }
26 |
27 | onRender() {
28 | const { pm, setState, editor } = this;
29 | setState({
30 | loading: true
31 | });
32 | setState({
33 | pages: [...pm.getAll()]
34 | });
35 | editor.on('page', () => {
36 | setState({
37 | pages: [...pm.getAll()]
38 | })
39 | });
40 | setState({
41 | loading: false
42 | });
43 | }
44 |
45 | isSelected(page) {
46 | return this.pm.getSelected().id === page.id;
47 | }
48 |
49 | selectPage(e) {
50 | this.pm.select(e.currentTarget.dataset.key);
51 | this.update();
52 | }
53 |
54 | removePage(e) {
55 | if (this.opts.confirmDeleteProject()) {
56 | this.pm.remove(e.currentTarget.dataset.key);
57 | this.update();
58 | }
59 | }
60 |
61 | openEdit(e) {
62 | const { editor } = this;
63 | this.setStateSilent({
64 | editablePageId: e.currentTarget.dataset.key
65 | });
66 | editor.Modal.close();
67 | editor.SettingsApp.setTab('page');
68 | editor.runCommand('open-settings');
69 | }
70 |
71 | editPage(id, name) {
72 | const currentPage = this.pm.get(id);
73 | currentPage?.set('name', name);
74 | this.update()
75 | }
76 |
77 | addPage() {
78 | const { pm } = this;
79 | const { nameText } = this.state
80 | if (!nameText) return;
81 | pm.add({
82 | name: nameText,
83 | component: ''
84 | });
85 | this.update();
86 | }
87 |
88 | handleNameInput(e) {
89 | this.setStateSilent({
90 | nameText: e.target.value.trim()
91 | })
92 | }
93 |
94 | renderPagesList() {
95 | const { pages, loading } = this.state;
96 | const { opts, isSelected } = this;
97 |
98 | if (loading) return opts.loader || 'Loading pages...
';
99 |
100 | return pages.map((page, i) => `
105 |
106 | ${page.get('name') || page.id}
107 | ${isSelected(page) || page.get('internal') ? '' : `⨯ `}
108 | ${page.get('internal') ? '' : ` `}
109 |
`).join("\n");
110 | }
111 |
112 | update() {
113 | this.$el?.find('.pages').html(this.renderPagesList());
114 | this.$el?.find('.page').on('click', this.selectPage);
115 | this.$el?.find('.page-edit').on('click', this.openEdit);
116 | this.$el?.find('.page-close').on('click', this.removePage);
117 | }
118 |
119 | render() {
120 | const { $, editor } = this;
121 |
122 | // Do stuff on render
123 | this.onRender();
124 | this.$el?.remove();
125 |
126 | const cont = $(`
127 |
128 | ${this.renderPagesList()}
129 |
130 |
131 |
136 |
137 |
138 | ${editor.I18n.t('grapesjs-project-manager.pages.new')}
139 |
140 |
`);
141 | cont.find('.add-page').on('click', this.addPage);
142 | cont.find('input').on('change', this.handleNameInput);
143 |
144 | this.$el = cont;
145 | return cont;
146 | }
147 |
148 | get findPanel() {
149 | return this.editor.Panels.getPanel('views-container');
150 | }
151 |
152 | showPanel() {
153 | this.state.isShowing = true;
154 | this.findPanel?.set('appendContent', this.render()).trigger('change:appendContent');
155 | this.update();
156 | }
157 |
158 | hidePanel() {
159 | this.state.isShowing = false;
160 | this.render();
161 | }
162 | }
--------------------------------------------------------------------------------
/src/manager/settings.js:
--------------------------------------------------------------------------------
1 | import UI from '../utils/ui';
2 |
3 | export default class SettingsApp extends UI {
4 | constructor(editor, opts = {}) {
5 | super(editor, opts);
6 | this.handleSave = this.handleSave.bind(this);
7 | this.handleThumbnail = this.handleThumbnail.bind(this);
8 | this.handleThumbnailInput = this.handleThumbnailInput.bind(this);
9 |
10 | /* Set initial app state */
11 | this.state = {
12 | tab: 'page',
13 | loading: false
14 | };
15 | }
16 |
17 | setTab(tab) {
18 | this.state.tab = tab;
19 | }
20 |
21 | update() {
22 | const { $el } = this;
23 | $el?.find('#settings').html(this.renderSettings());
24 | $el?.find('#generate').on('click', this.handleThumbnail);
25 | $el?.find('input#thumbnail').on('change', this.handleThumbnailInput);
26 | }
27 |
28 | onRender() {
29 | const { setState } = this;
30 | setState({
31 | loading: true
32 | });
33 | //? Setup code here
34 | setState({
35 | loading: false
36 | });
37 | }
38 |
39 | handleSave(e) {
40 | const { $el, editor } = this;
41 | const { tab } = this.state;
42 | if (tab === 'page') {
43 | const id = editor.PagesApp.editableId;
44 | const name = $el?.find('input.name').val().trim();
45 | id && editor.PagesApp.editPage(id, name);
46 | } else {
47 | const id = editor.TemplateManager.editableId;
48 | const thumbnail = $el?.find('input.thumbnail').val().trim();
49 | const name = $el?.find('input.name').val().trim();
50 | const description = $el?.find('input.desc').val().trim();
51 | const template = $el?.find('input.template').get(0).checked;
52 | id && editor.TemplateManager.handleEdit({ id, thumbnail, name, description, template });
53 | }
54 | editor.Modal.close();
55 | }
56 |
57 | handleThumbnail(e) {
58 | const { editor, $el, opts } = this;
59 | editor.runCommand('take-screenshot', {
60 | clb(dataUrl) {
61 | $el?.find('img').attr('src', dataUrl);
62 | opts.onThumbnail(dataUrl, $el?.find('input.thumbnail'));
63 | }
64 | })
65 | }
66 |
67 | handleThumbnailInput(e) {
68 | this.$el?.find('img').attr('src', e.target.value.trim());
69 | }
70 |
71 | renderSettings() {
72 | const { tab, loading } = this.state;
73 | const { opts, pfx, pm, editor } = this;
74 |
75 | if (loading) return opts.loader || 'Loading settings...
';
76 |
77 | if (tab === 'page') {
78 | const page = pm.get(editor.PagesApp.editableId);
79 | const value = page?.get('name') || page?.id || '';
80 | return `
81 | ${editor.I18n.t('grapesjs-project-manager.settings.labels.name')}
82 |
83 |
84 |
88 |
`
89 | } else {
90 | const clb = site => site.id === editor.TemplateManager.editableId;
91 | const site = editor.TemplateManager.allSites.find(clb);
92 | return `
93 | ${editor.I18n.t('grapesjs-project-manager.settings.help')}
94 |
95 |
96 | ${editor.I18n.t('grapesjs-project-manager.settings.labels.thumbnail')}
97 |
98 |
99 |
105 |
106 |
107 |
108 |
109 |
110 |
111 | ${editor.I18n.t('grapesjs-project-manager.settings.generate')}
112 |
113 |
114 |
115 | ${editor.I18n.t('grapesjs-project-manager.settings.labels.name')}
116 |
117 |
118 |
124 |
125 |
126 | ${editor.I18n.t('grapesjs-project-manager.settings.labels.description')}
127 |
128 |
129 |
135 |
136 |
137 |
138 |
139 | ${editor.I18n.t('grapesjs-project-manager.settings.labels.template')}
140 |
141 |
`
142 | }
143 | }
144 |
145 | render() {
146 | const { $, editor } = this;
147 |
148 | // Do stuff on render
149 | this.onRender();
150 | this.$el?.remove();
151 |
152 | const cont = $(`
153 |
154 | ${this.renderSettings()}
155 |
156 |
157 |
158 | ${editor.I18n.t('grapesjs-project-manager.settings.save')}
159 |
160 |
161 |
`);
162 | cont.find('#save').on('click', this.handleSave);
163 | cont.find('#generate').on('click', this.handleThumbnail);
164 | cont.find('input#thumbnail').on('change', this.handleThumbnailInput);
165 |
166 | this.$el = cont;
167 | return cont;
168 | }
169 | }
--------------------------------------------------------------------------------
/src/manager/templates.js:
--------------------------------------------------------------------------------
1 | import ago from '../utils/timeago';
2 | import UI from '../utils/ui';
3 | import objSize from '../utils/objsize';
4 | import { sortByDate, sortByName, sortByPages, sortBySize, matchText } from '../utils/sort';
5 |
6 | export default class TemplateManager extends UI {
7 | constructor(editor, opts = {}) {
8 | super(editor, opts);
9 | this.handleSort = this.handleSort.bind(this);
10 | this.handleFilterInput = this.handleFilterInput.bind(this);
11 | this.handleNameInput = this.handleNameInput.bind(this);
12 | this.handleOpen = this.handleOpen.bind(this);
13 | this.handleCreate = this.handleCreate.bind(this);
14 | this.handleDelete = this.handleDelete.bind(this);
15 | this.openEdit = this.openEdit.bind(this);
16 |
17 | /* Set initial app state */
18 | this.state = {
19 | editableProjectId: '',
20 | projectId: '',
21 | tab: 'pages',
22 | sites: [],
23 | nameText: '',
24 | filterText: '',
25 | loading: false,
26 | sortBy: 'published_at',
27 | sortOrder: 'desc'
28 | };
29 | }
30 |
31 | get editableId() {
32 | return this.state.editableProjectId;
33 | }
34 |
35 | get allSites() {
36 | return this.state.sites;
37 | }
38 |
39 | get allSitesSize() {
40 | return objSize(this.state.sites);
41 | }
42 |
43 | async onRender() {
44 | const { setState, cs } = this;
45 |
46 | /* Set request loading state */
47 | setState({
48 | loading: true
49 | });
50 |
51 | /* Fetch sites from storage API */
52 | const sites = await cs.loadAll();
53 | /* Set sites and turn off loading state */
54 | setState({
55 | sites,
56 | loading: false
57 | });
58 | }
59 |
60 | handleFilterInput(e) {
61 | this.setState({
62 | filterText: e.target.value.trim()
63 | });
64 | }
65 |
66 | handleNameInput(e) {
67 | this.setStateSilent({
68 | nameText: e.target.value.trim()
69 | })
70 | }
71 |
72 | handleSort(e) {
73 | const { sortOrder } = this.state;
74 | if (e.target && e.target.dataset) {
75 | this.setState({
76 | sortBy: e.target.dataset.sort,
77 | // invert sort order
78 | sortOrder: sortOrder === 'desc' ? 'asc' : 'desc'
79 | });
80 | }
81 | }
82 |
83 | handleTabs(e) {
84 | const { target } = e;
85 | const { $el, pfx, $ } = this;
86 | $el.find(`.${pfx}tablinks`).removeClass('active');
87 | $(target).addClass('active');
88 | if (target.id === 'pages') {
89 | this.setState({ tab: 'pages' });
90 | } else {
91 | this.setState({ tab: 'templates' });
92 | }
93 | }
94 |
95 | async handleOpen(e) {
96 | const { editor, cs } = this;
97 | const { projectId } = this.state;
98 | if (!projectId || projectId === cs.currentId) {
99 | this.opts.currentPageOpen()
100 | return;
101 | }
102 | cs.setId(projectId);
103 | const res = await editor.load();
104 | cs.setName(res.name);
105 | cs.setThumbnail(res.thumbnail || '');
106 | cs.setIsTemplate(res.template);
107 | cs.setDescription(res.description || 'No description');
108 | editor.Modal.close();
109 | }
110 |
111 | async handleCreate(e) {
112 | const { editor, cs } = this;
113 | const { projectId, nameText } = this.state;
114 | const id = editor.runCommand('get-uuidv4');
115 | const name = nameText || 'New-' + id.substring(0, 8);
116 | const def = {
117 | id,
118 | name,
119 | template: false,
120 | thumbnail: '',
121 | styles: '[]',
122 | description: 'No description',
123 | pages: `[{"id": "${crypto.randomUUID().substring(0, 13)}", "name": "index"}]`,
124 | styles: '[]',
125 | assets: '[]'
126 | };
127 | if (!projectId) {
128 | cs.setId(id);
129 | await cs.store(def);
130 | cs.setIsTemplate(false);
131 | const res = await editor.load();
132 | cs.setId(res.id);
133 | cs.setName(res.name);
134 | cs.setThumbnail(res.thumbnail || '');
135 | cs.setDescription(res.description || 'No description');
136 | editor.Modal.close();
137 | } else {
138 | cs.setId(projectId);
139 | cs.setIsTemplate(false);
140 | const res = await editor.load();
141 | cs.setId(id);
142 | cs.setName(name);
143 | cs.setThumbnail(res.thumbnail || '');
144 | cs.setDescription(res.description || 'No description');
145 | editor.Modal.close();
146 | }
147 | }
148 |
149 | openEdit(e) {
150 | const { editor, setStateSilent } = this;
151 | setStateSilent({
152 | editableProjectId: e.currentTarget.dataset.id
153 | });
154 | editor.Modal.close();
155 | editor.SettingsApp.setTab('project');
156 | editor.runCommand('open-settings');
157 | }
158 |
159 | handleEdit(data) {
160 | const { opts, cs, editor } = this;
161 | if (typeof opts.update === 'function') {
162 | opts.update({ ...data, updated_at: Date.now() }, editor);
163 | } else {
164 | opts.onUpdateAsync(cs.update({ ...data, updated_at: Date.now() }));
165 | }
166 | }
167 |
168 | async handleDelete(e) {
169 | const { cs, setState, opts } = this;
170 | if (opts.confirmDeleteProject()) {
171 | const toDel = e.currentTarget.dataset.id;
172 | if (typeof opts.delete === 'function') {
173 | opts.delete(toDel)
174 | } else {
175 | const res = await opts.onDeleteAsync(cs.delete(toDel));
176 | opts.onDelete(res);
177 | }
178 | const sites = await cs.loadAll();
179 | setState({ sites });
180 | }
181 | }
182 |
183 | renderSiteList() {
184 | const { sites, tab, filterText, loading, sortBy, sortOrder } = this.state;
185 | const { pfx, opts, cs, editor } = this;
186 |
187 | if (loading) return opts.loader || 'Loading sites...
';
188 |
189 | if (!sites.length) return opts.nosites || 'No Sites
';
190 |
191 | let order
192 | if (sortBy === 'id') {
193 | order = sortByName(sortBy, sortOrder);
194 | } else if (sortBy === 'updated_at' || sortBy === 'created_at') {
195 | order = sortByDate(sortBy, sortOrder);
196 | } else if (sortBy === 'pages') {
197 | order = sortByPages(sortBy, sortOrder);
198 | } else if (sortBy === 'size') {
199 | order = sortBySize(sortOrder);
200 | }
201 |
202 | const sortedSites = sites.sort(order);
203 |
204 | let matchingSites = sortedSites.filter(site => {
205 | // No search query. Show all
206 | if (!filterText && tab === 'pages') {
207 | return true;
208 | }
209 |
210 | const { id, name, template } = site;
211 | if (
212 | (matchText(filterText, id) ||
213 | matchText(filterText, name)) &&
214 | tab === 'pages'
215 | ) {
216 | return true;
217 | }
218 |
219 | if (tab === 'templates' && template) {
220 | return true;
221 | }
222 |
223 | // no match!
224 | return false;
225 | })
226 | .map((site, i) => {
227 | const {
228 | id,
229 | name,
230 | description,
231 | thumbnail,
232 | created_at,
233 | updated_at
234 | } = site;
235 | const size = objSize(site);
236 | const _pages = site.pages ? site.pages : (opts.legacyPrefix ? site[`${opts.legacyPrefix}pages`] : []);
237 | const pages = typeof _pages === 'string' ? JSON.parse(_pages) : _pages;
238 | const time = updated_at ? ago(updated_at) : 'NA';
239 | const createdAt = created_at ? ago(created_at) : 'NA';
240 | const pageNames = pages.map(page => page.name).join(', ');
241 | return `
246 |
247 |
248 |
249 |
250 |
251 | ${name}
252 |
253 |
254 | ${description}
255 |
256 |
257 |
${time}
258 |
259 |
260 | ${pages.length || 1}
261 |
262 |
263 |
${createdAt}
264 | ${opts.size ? `
265 | ${size.toFixed(2)} KB
266 |
` : ''}
267 |
268 |
269 | ${!(cs.currentId === id) ? ` ` : ''}
270 |
271 |
`;
272 | }).join('\n');
273 |
274 | if (!matchingSites.length) {
275 | if (tab === 'templates') return opts.nosites || 'No Templates Available.
';
276 | matchingSites = `
277 |
278 | No '${filterText}' examples found. Clear your search and try again.
279 |
280 | `;
281 | }
282 | return matchingSites;
283 | }
284 |
285 | renderSiteActions() {
286 | const { editor } = this;
287 |
288 | return this.state.tab === 'pages' ?
289 | `
290 |
294 |
295 | ${editor.I18n.t('grapesjs-project-manager.templates.open')}
296 |
297 |
` :
298 | `
299 | ${editor.I18n.t('grapesjs-project-manager.templates.help')}
300 |
301 |
302 |
306 |
307 | ${editor.I18n.t('grapesjs-project-manager.templates.create')}
308 |
309 |
`;
310 | }
311 |
312 | renderThumbnail(thumbnail, page) {
313 | const def = ` `;
314 | if (thumbnail) return def;
315 | else if (page.html) return `
316 |
317 |
318 | ${page.html + ''}
319 |
320 |
321 | `;
322 | return def;
323 | }
324 |
325 | update() {
326 | this.$el?.find('#site-list').html(this.renderSiteList());
327 | this.$el?.find('#tm-actions').html(this.renderSiteActions());
328 | const sites = this.$el?.find('.site-wrapper');
329 | const search = this.$el?.find('input.search');
330 | const name = this.$el?.find('input.name');
331 | this.setStateSilent({ projectId: '' });
332 | if (sites) {
333 | sites.on('click', e => {
334 | sites.removeClass('selected');
335 | this.$(e.currentTarget).addClass('selected');
336 | this.setStateSilent({ projectId: e.currentTarget.dataset.id });
337 | });
338 | }
339 | if (search) {
340 | search.val(this.state.filterText);
341 | search.on('change', this.handleFilterInput);
342 | }
343 | if (name) {
344 | name.val(this.state.nameText);
345 | name.on('change', this.handleNameInput);
346 | }
347 | this.$el?.find('#open').on('click', this.handleOpen);
348 | this.$el?.find('#create').on('click', this.handleCreate);
349 | this.$el?.find('i.edit').on('click', this.openEdit);
350 | this.$el?.find('i.delete').on('click', this.handleDelete);
351 | }
352 |
353 | render() {
354 | const { $, pfx, opts, editor } = this;
355 | const { tab } = this.state
356 |
357 | // Do stuff on render
358 | this.onRender();
359 | this.$el?.remove();
360 |
361 | /* Show admin UI */
362 | const cont = $(`
363 |
364 |
365 |
366 | ${editor.I18n.t('grapesjs-project-manager.templates.all')}
367 |
368 |
369 | ${editor.I18n.t('grapesjs-project-manager.templates.templates')}
370 |
371 |
372 |
373 | ${this.renderSiteActions()}
374 |
375 |
423 |
424 | ${this.renderSiteList()}
425 |
426 |
427 |
`);
428 | cont.find('.header').on('click', this.handleSort);
429 | cont.find('#pages, #templates').on('click', this.handleTabs);
430 |
431 | this.$el = cont;
432 | return cont;
433 | }
434 | }
--------------------------------------------------------------------------------
/src/storage/firestore.js:
--------------------------------------------------------------------------------
1 | import { storageFireStore, helpers } from '../consts';
2 |
3 | export default (editor, opts = {}) => {
4 | const sm = editor.StorageManager;
5 | const storageName = storageFireStore;
6 |
7 | let db;
8 | let doc;
9 | let collection;
10 | const { apiKey, authDomain, projectId } = opts;
11 | const dbSettings = opts.settings;
12 | const onError = err => sm.onError(storageName, err.code || err);
13 |
14 | const getDoc = () => doc;
15 |
16 | const getAsyncCollection = () => {
17 | if (collection) return collection;
18 | if (!firebase.apps.length) {
19 | firebase.initializeApp({ apiKey, authDomain, projectId, ...opts.firebaseConfig });
20 | db = firebase.firestore();
21 | db.settings(dbSettings);
22 | }
23 | else {
24 | firebase.app();
25 | db = firebase.firestore();
26 | db.settings(dbSettings);
27 | }
28 |
29 | if (opts.enableOffline) {
30 | db.enablePersistence().catch(onError);
31 | }
32 |
33 | collection = db.collection(opts.objectStoreName);
34 | return collection;
35 | };
36 |
37 | const getAsyncDoc = () => {
38 | const cll = getAsyncCollection();
39 | const cs = editor.Storage.getCurrentStorage();
40 | doc = cll.doc(cs.currentId);
41 | return doc;
42 | };
43 |
44 | sm.add(storageName, {
45 | ...helpers,
46 | getDoc,
47 |
48 | setDocId(id) {
49 | this.currentId = id;
50 | },
51 |
52 | async load(keys) {
53 | const _doc = getAsyncDoc();
54 | const doc = await _doc.get();
55 | return doc.exists ? doc.data() : {};
56 | },
57 |
58 | async loadAll() {
59 | const cll = getAsyncCollection();
60 | const docs = await cll.get();
61 | const data = [];
62 | docs.forEach(doc => data.push(doc.data()));
63 | return data;
64 | },
65 |
66 | async store(data) {
67 | const cll = getAsyncCollection();
68 | await cll.doc(data.id || this.currentId).set({
69 | id: this.currentId,
70 | name: this.currentName,
71 | template: this.isTemplate,
72 | thumbnail: this.currentThumbnail,
73 | description: this.description,
74 | updated_at: Date.now(),
75 | ...data
76 | });
77 | },
78 |
79 | async update(data) {
80 | const { id, ..._data } = data;
81 | const cll = getAsyncCollection();
82 | await cll.doc(id).set(_data, { merge: true });
83 | },
84 |
85 | async delete(index) {
86 | if (!index) {
87 | const _doc = getAsyncDoc();
88 | await _doc.delete();
89 | } else {
90 | const cll = getAsyncCollection();
91 | await cll.doc(index).delete();
92 | }
93 | }
94 | });
95 | }
--------------------------------------------------------------------------------
/src/storage/index.js:
--------------------------------------------------------------------------------
1 | import indexeddb from './indexeddb';
2 | import remote from './remote';
3 | import firestore from './firestore';
4 |
5 | export default (editor, opts = {}) => {
6 | // Load indexeddb storage
7 | indexeddb(editor, opts);
8 |
9 | // Load remote storage
10 | remote(editor, opts);
11 |
12 | // Load firestore storage
13 | firestore(editor, opts);
14 | }
--------------------------------------------------------------------------------
/src/storage/indexeddb.js:
--------------------------------------------------------------------------------
1 | import { storageIDB, helpers } from '../consts';
2 |
3 | export default (editor, opts = {}) => {
4 | let db;
5 | const sm = editor.StorageManager;
6 | const storageName = storageIDB;
7 | const objsName = opts.objectStoreName;
8 |
9 | // Functions for DB retrieving
10 | const getDb = () => db;
11 | const getAsyncDb = () => new Promise((resolve, reject) => {
12 | if (db) {
13 | resolve(db);
14 | } else {
15 | const indexedDB = window.indexedDB || window.mozIndexedDB ||
16 | window.webkitIndexedDB || window.msIndexedDB;
17 | const request = indexedDB.open(opts.dbName, opts.indexeddbVersion);
18 | request.onerror = reject;
19 | request.onsuccess = () => {
20 | db = request.result;
21 | db.onerror = reject;
22 | resolve(db);
23 | };
24 | request.onupgradeneeded = e => {
25 | const objs = request.result.createObjectStore(objsName, { keyPath: 'id' });
26 | objs.createIndex('name', 'name', { unique: false });
27 | };
28 | }
29 | });
30 |
31 | // Functions for object store retrieving
32 | const getObjectStore = () => {
33 | return db.transaction([objsName], 'readwrite').objectStore(objsName);
34 | };
35 | const getAsyncObjectStore = async () => {
36 | if (db) {
37 | return getObjectStore();
38 | } else {
39 | await getAsyncDb();
40 | return getObjectStore();
41 | }
42 | };
43 |
44 | // Add custom storage to the editor
45 | sm.add(storageName, {
46 | ...helpers,
47 | getDb,
48 |
49 | getObjectStore,
50 |
51 | async load(keys) {
52 | const objs = await getAsyncObjectStore();
53 | return new Promise(
54 | (resolve, reject) => {
55 | const request = objs.get(this.currentId);
56 | request.onerror = reject;
57 | request.onsuccess = () => {
58 | resolve(request.result || {});
59 | };
60 | }
61 | );
62 | },
63 |
64 | async loadAll() {
65 | const objs = await getAsyncObjectStore();
66 | return new Promise(
67 | (resolve, reject) => {
68 | const request = objs.getAll();
69 | request.onerror = reject;
70 | request.onsuccess = () => {
71 | resolve(request.result || []);
72 | };
73 | }
74 | );
75 | },
76 |
77 | async store(data) {
78 | const objs = await getAsyncObjectStore();
79 | return new Promise(
80 | (resolve, reject) => {
81 | const request = objs.put({
82 | id: this.currentId,
83 | name: this.currentName,
84 | template: this.isTemplate,
85 | thumbnail: this.currentThumbnail,
86 | description: this.description,
87 | updated_at: Date.now(),
88 | ...data
89 | });
90 | request.onerror = reject;
91 | request.onsuccess = () => {
92 | resolve(request.result);
93 | };
94 | }
95 | );
96 | },
97 |
98 | async update(data) {
99 | const { id, ..._data } = data;
100 | const objs = await getAsyncObjectStore();
101 | return new Promise(
102 | (resolve, reject) => {
103 | const request = objs.get(id);
104 | request.onerror = reject;
105 | request.onsuccess = () => {
106 | objs.put({ id, ...request.result, ..._data });
107 | resolve(request.result);
108 | };
109 | }
110 | );
111 | },
112 |
113 | async delete(index) {
114 | const objs = await getAsyncObjectStore();
115 | return new Promise(
116 | (resolve, reject) => {
117 | const request = objs.delete(index || this.currentId);
118 | request.onerror = reject;
119 | request.onsuccess = () => {
120 | resolve(request.result);
121 | };;
122 | }
123 | );
124 | }
125 | });
126 | }
--------------------------------------------------------------------------------
/src/storage/remote.js:
--------------------------------------------------------------------------------
1 | import { storageRemote, helpers } from '../consts';
2 |
3 | export default (editor, opts = {}) => {
4 | const sm = editor.StorageManager;
5 | const storageName = storageRemote;
6 | const remote = sm.get('remote');
7 | const stOpts = sm.getStorageOptions('remote');
8 |
9 | // Add custom storage to the editor
10 | sm.add(storageName, {
11 | ...helpers,
12 |
13 | async load(keys = {}) {
14 | const { urlLoad } = stOpts;
15 | const id = urlLoad.endsWith('/') ? this.currentId : `/${this.currentId}`;
16 | const projectData = await remote.load({
17 | ...stOpts,
18 | ...{ urlLoad: urlLoad + id },
19 | ...keys
20 | });
21 | return projectData;
22 | },
23 |
24 | async loadAll(keys = {}) {
25 | return await remote.load({ ...stOpts, ...keys });
26 | },
27 |
28 | async store(data, keys = {}) {
29 | const { urlStore } = stOpts;
30 | const id = urlStore.endsWith('/') ? this.currentId : `/${this.currentId}`;
31 | const projectData = await remote.store({
32 | id: this.currentId,
33 | name: this.currentName,
34 | template: this.isTemplate,
35 | thumbnail: this.currentThumbnail,
36 | description: this.description,
37 | updated_at: Date.now(),
38 | ...data
39 | }, {
40 | ...stOpts,
41 | ...{ urlStore: opts.uuidInPath ? urlStore + id : urlStore },
42 | ...keys
43 | });
44 | return projectData;
45 | },
46 |
47 | async update(data, keys = {}) {
48 | const { urlStore } = stOpts;
49 | let { id } = data;
50 | id = urlStore.endsWith('/') ? id : `/${id}`;
51 | const projectData = await remote.store(data, {
52 | ...stOpts,
53 | ...{ urlStore: urlStore + id },
54 | ...keys
55 | });
56 | return projectData;
57 | },
58 |
59 | async delete(index, keys = {}) {
60 | const { urlDelete } = stOpts;
61 | let id = index || this.currentId;
62 | id = urlDelete.endsWith('/') ? id : `/${id}`;
63 | const res = await remote.request(urlDelete + id, { method: 'delete', ...keys });
64 | return res;
65 | }
66 | });
67 | }
--------------------------------------------------------------------------------
/src/styles.scss:
--------------------------------------------------------------------------------
1 | /* Class names prefixes */
2 | $prefix: 'gjs-' !default;
3 | /* Main color */
4 | $mainColor: #936a9b;
5 |
6 | :root {
7 | --border-color: rgba(0, 0, 0, 0.15);
8 | --background-color: #3c4a49;
9 |
10 | --background-box-title: #263332;
11 | }
12 |
13 | .app {
14 | margin: 20px;
15 | margin-top: 30px;
16 | animation: fadein .3s;
17 |
18 | a {
19 | text-decoration: none;
20 | color: $mainColor;
21 | }
22 |
23 | h1 {
24 | margin-bottom: 30px;
25 | }
26 |
27 | h2 {
28 | color: $mainColor;
29 | }
30 |
31 | button {
32 | cursor: pointer;
33 | background-color: #424242;
34 | font-family: inherit;
35 | color: #fff;
36 | display: inline-flex;
37 | align-items: center;
38 | justify-content: center;
39 | font-size: 15px;
40 | border: 1px solid #e9ebeb;
41 | border-bottom-color: #e1e3e3;
42 | border-radius: 4px;
43 | background-color: #fff;
44 | color: rgba(14, 30, 37, .87);
45 | box-shadow: 0 2px 4px 0 rgba(14, 30, 37, .12);
46 | transition: all .2s ease;
47 | transition-property: background-color, color, border, box-shadow;
48 | outline: 0;
49 | font-weight: 500;
50 | background: $mainColor;
51 | color: #fff;
52 | border-color: transparent;
53 | }
54 | }
55 |
56 | .title-inner {
57 | display: flex;
58 | align-items: center;
59 |
60 | button {
61 | margin-left: 20px;
62 | }
63 | }
64 |
65 | .primary-button {
66 | padding: 13px 18px;
67 |
68 | &:hover {
69 | background: #73308d;
70 | }
71 | }
72 |
73 | .flex-row {
74 | display: flex;
75 | justify-content: space-between;
76 | align-items: center;
77 |
78 | button {
79 | height: 45px;
80 | }
81 | }
82 |
83 | .tm-input {
84 | border-radius: 2px;
85 | font-size: 16px;
86 | padding: 11px 15px;
87 | min-width: 300px;
88 | display: inline-block;
89 | box-shadow: 0 0 0 2px rgba(120, 130, 152, .25);
90 | border: none;
91 | outline: none;
92 | transition: all .3s ease;
93 | margin: 20px 0;
94 | color: white;
95 | background-color: rgba(0, 0, 0, 0.2);
96 |
97 | &:active,
98 | &:focus,
99 | &:hover {
100 | box-shadow: 0 0 0 2px $mainColor;
101 | }
102 |
103 | &.sm {
104 | width: 100%;
105 | min-width: 0;
106 | font-size: 12px;
107 | margin: 10px 0;
108 | padding: 8px 10px;
109 | }
110 | }
111 |
112 | .header {
113 | cursor: pointer;
114 | font-weight: bold;
115 | font-size: 16px !important;
116 | }
117 |
118 | .item {
119 | text-align: center;
120 | }
121 |
122 | #site-list {
123 | max-height: 400px;
124 | overflow-y: auto;
125 | overflow-x: hidden;
126 | }
127 |
128 | .site-wrapper,
129 | .site-wrapper-header {
130 | display: flex;
131 | align-items: center;
132 | padding: 10px 0;
133 | padding-left: 20px;
134 | font-size: 14px;
135 | transition: .3s;
136 | }
137 |
138 | .site-wrapper.selected {
139 | border: 2px solid $mainColor;
140 | }
141 |
142 | .site-wrapper.open,
143 | .site-wrapper:nth-child(even).open {
144 | background-color: rgba(147, 106, 155, .2);
145 | }
146 |
147 | .site-wrapper-header {
148 | padding-left: 15px;
149 | border-bottom: 2px solid rgba(14, 30, 37, .2);
150 | }
151 |
152 | .site-wrapper:hover,
153 | .site-wrapper:nth-child(even):hover {
154 | background: rgba(255, 255, 255, .1);
155 | }
156 |
157 | .site-wrapper-header,
158 | .site-wrapper:nth-child(even) {
159 | background-color: rgba(0, 0, 0, .1);
160 | }
161 |
162 | .site-screenshot,
163 | .site-screenshot-header {
164 | height: 64px;
165 | margin: 0 24px 0 0;
166 | min-width: 102.4px;
167 | position: relative;
168 | overflow: hidden;
169 | }
170 |
171 | .site-screenshot-header {
172 | height: 30px;
173 | }
174 |
175 | .site-screenshot:before {
176 | background: #dadcdd;
177 | bottom: 0;
178 | content: " ";
179 | left: 0;
180 | position: absolute;
181 | right: 0;
182 | top: 0;
183 | }
184 |
185 | .site-screenshot a {
186 | display: block;
187 | position: relative;
188 | z-index: 9;
189 | height: 100%;
190 | }
191 |
192 | .site-screenshot img {
193 | position: relative;
194 | width: 100%;
195 | width: 102.4px;
196 | border: none;
197 | }
198 |
199 | .site-screenshot img[alt]:after {
200 | display: block;
201 | position: absolute;
202 | top: 0;
203 | left: 0;
204 | width: 100%;
205 | height: 100%;
206 | background-color: #dadcdd;
207 | font-weight: 300;
208 | line-height: 2;
209 | text-align: center;
210 | content: '';
211 | }
212 |
213 | .site-info {
214 | min-width: 250px;
215 | max-width: 250px;
216 |
217 | h2 {
218 | margin: 0px;
219 | margin-bottom: 5px;
220 | }
221 |
222 | a {
223 | text-decoration: none;
224 | }
225 | }
226 |
227 | .site-meta a {
228 | font-size: 14px;
229 | color: rgba(165, 210, 230, 0.8);
230 | }
231 |
232 | .site-update-time {
233 | min-width: 150px;
234 | }
235 |
236 | .site-create-time {
237 | min-width: 130px;
238 | }
239 |
240 | .site-pages,
241 | .site-size {
242 | min-width: 80px;
243 | }
244 |
245 | .site-actions {
246 | min-width: 200px;
247 |
248 | i {
249 | color: $mainColor;
250 | background-color: rgba(0, 0, 0, 0.1);
251 | border: 1px solid $mainColor;
252 | border-radius: 2px;
253 | padding: 5px;
254 | cursor: pointer;
255 |
256 | &:hover {
257 | background-color: $mainColor;
258 | color: white;
259 | }
260 | }
261 | }
262 |
263 | .#{$prefix}templates-overlay {
264 | position: absolute;
265 | pointer-events: none;
266 | top: 0;
267 | width: 100%;
268 | height: 100%;
269 | background-color: rgba(255, 255, 255, 0.05);
270 | }
271 |
272 | .#{$prefix}mdl-dialog-tml {
273 | max-width: 1000px;
274 | margin-top: 45px;
275 | }
276 |
277 | .#{$prefix}mdl-dialog-md {
278 | max-width: 400px;
279 | margin-top: 45px;
280 | }
281 |
282 | .#{$prefix}tip-about {
283 | padding: 10px;
284 | font-size: .9rem;
285 | border: 1px solid rgba(0, 0, 0, 0.2);
286 | border-left: 3px solid $mainColor;
287 | background-color: rgba(0, 0, 0, 0.1);
288 | margin-bottom: 10px;
289 | }
290 |
291 | .#{$prefix}tab {
292 | overflow: hidden;
293 | border-bottom: 1px solid rgba(0, 0, 0, 0.2);
294 | margin-bottom: 10px;
295 |
296 | button {
297 | background-color: inherit;
298 | color: inherit;
299 | float: left;
300 | border: 1px solid rgba(0, 0, 0, 0.1);
301 | outline: none;
302 | cursor: pointer;
303 | padding: 14px 16px;
304 | transition: .3s;
305 |
306 | &:hover {
307 | background-color: rgba(0, 0, 0, 0.1);
308 | }
309 |
310 | &.active {
311 | background-color: rgba(0, 0, 0, 0.2);
312 | }
313 | }
314 | }
315 |
316 | .pages-wrp,
317 | .pages {
318 | display: flex;
319 | flex-direction: column;
320 | }
321 |
322 | .pages-wrp {
323 | padding: 5px;
324 | }
325 |
326 | .pages-title {
327 | padding: 5px;
328 | margin: 0;
329 | border-bottom: 1px solid rgba(0, 0, 0, .1);
330 | }
331 |
332 | .add-page {
333 | background: $mainColor;
334 | color: white;
335 | font-size: 12px;
336 | padding: 8px 5px;
337 | border-radius: 2px;
338 | cursor: pointer;
339 | white-space: nowrap;
340 | margin-bottom: 10px;
341 | }
342 |
343 | .page {
344 | font-size: 12px;
345 | text-align: left;
346 | padding: 5px;
347 | margin-bottom: 5px;
348 | border-radius: 2px;
349 | border: 1px solid rgba(0, 0, 0, .2);
350 | box-shadow: 0 1px 0 0 rgb(0, 0, 0, .15);
351 | transition: all .2s ease 0s;
352 | transition-property: box-shadow, color;
353 | cursor: pointer;
354 |
355 | &:hover {
356 | color: $mainColor;
357 | box-shadow: 0 3px 4px 0 rgb(0, 0, 0, .15);
358 | }
359 |
360 | &.selected {
361 | color: $mainColor;
362 | border: 1px solid $mainColor;
363 | background-color: rgba(0, 0, 0, .1);
364 | }
365 | }
366 |
367 | .page-edit,
368 | .page-close {
369 | opacity: .5;
370 | float: right;
371 | background-color: rgba(0, 0, 0, .1);
372 | border: 1px solid rgba(0, 0, 0, .2);
373 | height: 17px;
374 | width: 17px;
375 | text-align: center;
376 | border-radius: 3px;
377 |
378 | &:hover {
379 | opacity: 1;
380 | }
381 | }
382 |
383 | .page-edit {
384 | margin-right: 5px;
385 | }
386 |
387 | .group {
388 | margin-bottom: 15px;
389 |
390 | input {
391 | padding: 0;
392 | height: initial;
393 | width: initial;
394 | margin-bottom: 0;
395 | display: none;
396 | cursor: pointer;
397 |
398 | &:checked+label:after {
399 | content: '';
400 | display: block;
401 | position: absolute;
402 | top: 2px;
403 | left: 9px;
404 | width: 6px;
405 | height: 14px;
406 | border: solid $mainColor;
407 | border-width: 0 2px 2px 0;
408 | transform: rotate(45deg);
409 | }
410 | }
411 |
412 | label {
413 | position: relative;
414 | cursor: pointer;
415 |
416 | &:before {
417 | content: '';
418 | -webkit-appearance: none;
419 | background-color: transparent;
420 | border: 2px solid $mainColor;
421 | box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05), inset 0px -15px 10px -12px rgba(0, 0, 0, 0.05);
422 | padding: 10px;
423 | display: inline-block;
424 | position: relative;
425 | vertical-align: middle;
426 | cursor: pointer;
427 | margin-right: 5px;
428 | }
429 | }
430 | }
431 |
432 | @keyframes fadein {
433 | 0% {
434 | opacity: 0;
435 | }
436 |
437 | 100% {
438 | opacity: 1;
439 | }
440 | }
--------------------------------------------------------------------------------
/src/utils/objsize.js:
--------------------------------------------------------------------------------
1 | export function objSizeMegaBytes(obj) {
2 | return objSizeBytes(obj) / (1024 * 1024);
3 | }
4 |
5 | export default function objSizeKiloBytes(obj) {
6 | return objSizeBytes(obj) / 1024;
7 | }
8 |
9 | export function objSizeBytes(obj) {
10 | return new TextEncoder().encode(JSON.stringify(obj)).length;
11 | }
--------------------------------------------------------------------------------
/src/utils/sort.js:
--------------------------------------------------------------------------------
1 | // https://github.com/netlify-labs/oauth-example/blob/master/src/utils/sort.js
2 | // License MIT
3 | import objSize from './objsize';
4 |
5 | export function matchText(search, text) {
6 | if (!text || !search) {
7 | return false
8 | }
9 | return text.toLowerCase().indexOf(search.toLowerCase()) > -1
10 | }
11 |
12 | export function sortByDate(dateType, order) {
13 | return function (a, b) {
14 | const timeA = new Date(a[dateType]).getTime()
15 | const timeB = new Date(b[dateType]).getTime()
16 | if (order === 'asc') {
17 | return timeA - timeB
18 | }
19 | // default 'desc' descending order
20 | return timeB - timeA
21 | }
22 | }
23 |
24 | export function sortByName(key, order) {
25 | return function (a, b) {
26 | if (order === 'asc') {
27 | if (a[key] < b[key]) return -1
28 | if (a[key] > b[key]) return 1
29 | }
30 | if (a[key] > b[key]) return -1
31 | if (a[key] < b[key]) return 1
32 | return 0
33 | }
34 | }
35 |
36 | export function sortByPages(key, order) {
37 | return function (a, b) {
38 | const pagesA = JSON.parse(a[key]);
39 | const pagesB = JSON.parse(b[key]);
40 | if (order === 'desc') {
41 | if (pagesA.length < pagesB.length) return -1
42 | if (pagesA.length > pagesB.length) return 1
43 | }
44 | if (pagesA.length > pagesB.length) return -1
45 | if (pagesA.length < pagesB.length) return 1
46 | return 0
47 | }
48 | }
49 |
50 | export function sortBySize(order) {
51 | return function (a, b) {
52 | const sizeA = objSize(a);
53 | const sizeB = objSize(b);
54 | if (order === 'asc') {
55 | if (sizeA < sizeB) return -1
56 | if (sizeA > sizeB) return 1
57 | }
58 | if (sizeA > sizeB) return -1
59 | if (sizeA < sizeB) return 1
60 | return 0
61 | }
62 | }
--------------------------------------------------------------------------------
/src/utils/timeago.js:
--------------------------------------------------------------------------------
1 | export default function timeago(date, short) {
2 | const obj = {
3 | second: 1000,
4 | minute: 60 * 1000,
5 | hour: 60 * 1000 * 60,
6 | day: 24 * 60 * 1000 * 60,
7 | week: 7 * 24 * 60 * 1000 * 60,
8 | month: 30 * 24 * 60 * 1000 * 60,
9 | year: 365 * 24 * 60 * 1000 * 60,
10 | }
11 |
12 | const { round } = Math,
13 | dir = ' ago',
14 | pl = function (v, n) {
15 | return (short === undefined) ? `${n} ${v + (n > 1 ? 's' : '') + dir}` : n + v.substring(0, 1)
16 | },
17 | ts = Date.now() - new Date(date).getTime();
18 | let ii;
19 |
20 | if (ts < 0) {
21 | ts *= -1;
22 | dir = ' from now';
23 | }
24 |
25 | for (var i in obj) {
26 | if (round(ts) < obj[i]) return pl(ii || 'm', round(ts / (obj[ii] || 1)))
27 | ii = i;
28 | }
29 | return pl(i, round(ts / obj[i]));
30 | }
--------------------------------------------------------------------------------
/src/utils/ui.js:
--------------------------------------------------------------------------------
1 | export default class UI {
2 | constructor(editor, opts = {}) {
3 | this.editor = editor;
4 | this.$ = editor.$;
5 | this.pfx = editor.getConfig('stylePrefix');
6 | this.opts = opts;
7 | this.setState = this.setState.bind(this);
8 | this.setStateSilent = this.setStateSilent.bind(this);
9 | this.onRender = this.onRender.bind(this);
10 | this.handleTabs = this.handleTabs.bind(this);
11 | }
12 |
13 | setState(state) {
14 | this.state = { ...this.state, ...state };
15 | this.update();
16 | }
17 |
18 | setStateSilent(state) {
19 | this.state = { ...this.state, ...state };
20 | }
21 |
22 | get sm() {
23 | return this.editor.Storage;
24 | }
25 |
26 | get cs() {
27 | return this.editor.Storage.getCurrentStorage();
28 | }
29 |
30 | get pm() {
31 | return this.editor.Pages;
32 | }
33 |
34 | onRender() { }
35 |
36 | handleTabs() { }
37 | }
--------------------------------------------------------------------------------