mirror of https://github.com/artf/grapesjs.git
committed by
GitHub
29 changed files with 993 additions and 1240 deletions
@ -1,67 +1,93 @@ |
|||||
export default { |
export default { |
||||
// Prefix identifier that will be used inside storing and loading
|
// Prefix identifier that will be used inside storing and loading
|
||||
|
// @deprecated
|
||||
id: 'gjs-', |
id: 'gjs-', |
||||
|
|
||||
|
// Default storage type. Available: local | remote
|
||||
|
type: 'local', |
||||
|
|
||||
// Enable/Disable autosaving
|
// Enable/Disable autosaving
|
||||
autosave: true, |
autosave: true, |
||||
|
|
||||
// Indicates if load data inside editor after init
|
// Enable/Disable autoload of data on editor init
|
||||
autoload: true, |
autoload: true, |
||||
|
|
||||
// Indicates which storage to use. Available: local | remote
|
/** |
||||
type: 'local', |
* In case the `remote` storage is selected, and this options is enabled, the project |
||||
|
* will be stored on the `local` storage in case the remote one fails. |
||||
|
* The local data are cleared on every sucessful remote save. When the remote storage |
||||
|
* fails (eg. network issue) and the editor is reloaded, a dialog with the possibility to |
||||
|
* recovery previous data will be shown. |
||||
|
* @example |
||||
|
* // Enable recovery with default confirm dialog
|
||||
|
* recovery: true, |
||||
|
* // Enable recovery with a custom dialog
|
||||
|
* recovery: (accept, cancel, editor) => { |
||||
|
* confirm('Recover data?') ? accept() : cancel(); |
||||
|
* }, |
||||
|
*/ |
||||
|
recovery: false, |
||||
|
|
||||
// If autosave enabled, indicates how many steps (general changes to structure)
|
// If autosave enabled, indicates how many steps (general changes to structure)
|
||||
// need to be done before save. Useful with remoteStorage to reduce remote calls
|
// need to be done before save. Useful with remoteStorage to reduce remote calls
|
||||
stepsBeforeSave: 1, |
stepsBeforeSave: 1, |
||||
|
|
||||
//Enable/Disable components model (JSON format)
|
/** |
||||
storeComponents: true, |
* Callback triggered before the store call (can be asynchronous). |
||||
|
* This can be used to enrich the project data to store. |
||||
//Enable/Disable styles model (JSON format)
|
*/ |
||||
storeStyles: true, |
onStore: data => data, |
||||
|
|
||||
//Enable/Disable saving HTML template
|
/** |
||||
storeHtml: true, |
* Callback triggered after the load call (can be asynchronous). |
||||
|
*/ |
||||
//Enable/Disable saving CSS template
|
onLoad: data => data, |
||||
storeCss: true, |
|
||||
|
// Default storage options
|
||||
// ONLY FOR LOCAL STORAGE
|
options: { |
||||
// If enabled, checks if browser supports Local Storage
|
local: { |
||||
checkLocal: true, |
key: 'gjsProject', |
||||
|
|
||||
// ONLY FOR REMOTE STORAGE
|
// If enabled, checks if browser supports LocalStorage
|
||||
// Custom parameters to pass with the remote storage request, eg. csrf token
|
checkLocal: true, |
||||
params: {}, |
}, |
||||
|
remote: { |
||||
// Custom headers for the remote storage request
|
// Custom headers
|
||||
headers: {}, |
headers: {}, |
||||
|
|
||||
// Endpoint where to save all stuff
|
// Endpoint URL where to store data project
|
||||
urlStore: '', |
urlStore: '', |
||||
|
|
||||
// Endpoint where to fetch data
|
// Endpoint URL where to load data project
|
||||
urlLoad: '', |
urlLoad: '', |
||||
|
|
||||
//Callback before request
|
// set contentType paramater of $.ajax
|
||||
beforeSend(jqXHR, settings) {}, |
// true: application/json; charset=utf-8'
|
||||
|
// false: 'x-www-form-urlencoded'
|
||||
//Callback after request
|
contentTypeJson: true, |
||||
onComplete(jqXHR, status) {}, |
|
||||
|
// Pass custom options to fetch API (remote storage)
|
||||
// set contentType paramater of $.ajax
|
// You can pass a simple object: { someOption: 'someValue' }
|
||||
// true: application/json; charset=utf-8'
|
// or a function which returns and object to add:
|
||||
// false: 'x-www-form-urlencoded'
|
// currentOpts => {
|
||||
contentTypeJson: true, |
// return currentOpts.method === 'POST' ? { method: 'PATCH' } : {};
|
||||
|
// }
|
||||
credentials: 'include', |
fetchOptions: '', |
||||
|
|
||||
// Pass custom options to fetch API (remote storage)
|
credentials: 'include', |
||||
// You can pass a simple object: { someOption: 'someValue' }
|
|
||||
// or a function which returns and object to add:
|
/** |
||||
// currentOpts => {
|
* The remote storage sends the project data as a body of the request. |
||||
// return currentOpts.method === 'post' ? { method: 'patch' } : {};
|
* You can use this method to update the body before the store call in order to align |
||||
// }
|
* with your API requirements. |
||||
fetchOptions: '', |
*/ |
||||
|
onStore: data => data, |
||||
|
|
||||
|
/** |
||||
|
* The remote storage loads the project data directly from the request response. |
||||
|
* You can use this method to properly extract the project data from the response. |
||||
|
*/ |
||||
|
onLoad: result => result, |
||||
|
}, |
||||
|
}, |
||||
}; |
}; |
||||
|
|||||
@ -1,62 +1,28 @@ |
|||||
import { Model } from 'backbone'; |
|
||||
import { hasWin } from 'utils/mixins'; |
import { hasWin } from 'utils/mixins'; |
||||
|
|
||||
export default Model.extend({ |
export default class LocalStorage { |
||||
defaults: { |
async store(data, opts = {}) { |
||||
checkLocal: true |
if (this.hasLocal(opts, true)) { |
||||
}, |
localStorage.setItem(opts.key, JSON.stringify(data)); |
||||
|
|
||||
/** |
|
||||
* @private |
|
||||
*/ |
|
||||
store(data, clb = () => {}) { |
|
||||
if (this.hasLocal()) { |
|
||||
for (let key in data) localStorage.setItem(key, data[key]); |
|
||||
} |
} |
||||
|
} |
||||
|
|
||||
clb && clb(); |
async load(opts = {}) { |
||||
}, |
let result = {}; |
||||
|
|
||||
/** |
|
||||
* @private |
|
||||
*/ |
|
||||
load(keys, clb = () => {}) { |
|
||||
const result = {}; |
|
||||
|
|
||||
if (this.hasLocal()) { |
if (this.hasLocal(opts, true)) { |
||||
for (let i = 0, len = keys.length; i < len; i++) { |
result = JSON.parse(localStorage.getItem(opts.key) || '{}'); |
||||
const value = localStorage.getItem(keys[i]); |
|
||||
if (value) result[keys[i]] = value; |
|
||||
} |
|
||||
} |
} |
||||
|
|
||||
clb && clb(result); |
|
||||
|
|
||||
return result; |
return result; |
||||
}, |
} |
||||
|
|
||||
/** |
|
||||
* @private |
|
||||
*/ |
|
||||
remove(keys) { |
|
||||
if (!this.hasLocal()) return; |
|
||||
|
|
||||
for (let i = 0, len = keys.length; i < len; i++) |
|
||||
localStorage.removeItem(keys[i]); |
|
||||
}, |
|
||||
|
|
||||
/** |
|
||||
* Check storage environment |
|
||||
* @private |
|
||||
* */ |
|
||||
hasLocal() { |
|
||||
const win = hasWin(); |
|
||||
|
|
||||
if (this.get('checkLocal') && (!win || !localStorage)) { |
hasLocal(opts = {}, thr) { |
||||
win && console.warn("Your browser doesn't support localStorage"); |
if (opts.checkLocal && (!hasWin() || !localStorage)) { |
||||
|
if (thr) throw new Error('localStorage not available'); |
||||
return false; |
return false; |
||||
} |
} |
||||
|
|
||||
return true; |
return true; |
||||
} |
} |
||||
}); |
} |
||||
|
|||||
@ -1,146 +1,66 @@ |
|||||
import Backbone from 'backbone'; |
|
||||
import fetch from 'utils/fetch'; |
import fetch from 'utils/fetch'; |
||||
import { isUndefined, isFunction } from 'underscore'; |
import { isUndefined, isFunction, isString } from 'underscore'; |
||||
|
|
||||
export default Backbone.Model.extend({ |
export default class RemoteStorage { |
||||
fetch, |
async store(data, opts = {}) { |
||||
|
await this.request(opts.urlStore, this.__props(opts, data), opts); |
||||
defaults: { |
} |
||||
urlStore: '', |
|
||||
urlLoad: '', |
|
||||
params: {}, |
|
||||
beforeSend() {}, |
|
||||
onComplete() {}, |
|
||||
contentTypeJson: false, |
|
||||
credentials: 'include', |
|
||||
fetchOptions: '' |
|
||||
}, |
|
||||
|
|
||||
/** |
|
||||
* Triggered before the request is started |
|
||||
* @private |
|
||||
*/ |
|
||||
onStart() { |
|
||||
const em = this.get('em'); |
|
||||
const before = this.get('beforeSend'); |
|
||||
before && before(); |
|
||||
}, |
|
||||
|
|
||||
/** |
|
||||
* Triggered on request error |
|
||||
* @param {Object} err Error |
|
||||
* @param {Function} [clbErr] Error callback |
|
||||
* @private |
|
||||
*/ |
|
||||
onError(err, clbErr) { |
|
||||
if (clbErr) { |
|
||||
clbErr(err); |
|
||||
} else { |
|
||||
const em = this.get('em'); |
|
||||
console.error(err); |
|
||||
em && em.trigger('storage:error', err); |
|
||||
} |
|
||||
}, |
|
||||
|
|
||||
/** |
|
||||
* Triggered on request response |
|
||||
* @param {string} text Response text |
|
||||
* @private |
|
||||
*/ |
|
||||
onResponse(text, clb) { |
|
||||
const em = this.get('em'); |
|
||||
const complete = this.get('onComplete'); |
|
||||
const typeJson = this.get('contentTypeJson'); |
|
||||
const parsable = text && typeof text === 'string'; |
|
||||
const res = typeJson && parsable ? JSON.parse(text) : text; |
|
||||
complete && complete(res); |
|
||||
clb && clb(res); |
|
||||
em && em.trigger('storage:response', res); |
|
||||
}, |
|
||||
|
|
||||
store(data, clb, clbErr) { |
|
||||
const body = {}; |
|
||||
|
|
||||
for (let key in data) { |
|
||||
body[key] = data[key]; |
|
||||
} |
|
||||
|
|
||||
this.request(this.get('urlStore'), { body }, clb, clbErr); |
async load(opts) { |
||||
}, |
return await this.request(opts.urlLoad, this.__props(opts), opts); |
||||
|
} |
||||
|
|
||||
load(keys, clb, clbErr) { |
request(url, props = {}, opts = {}) { |
||||
this.request(this.get('urlLoad'), { method: 'get' }, clb, clbErr); |
return fetch(url, props) |
||||
}, |
.then(res => { |
||||
|
const result = res.text(); |
||||
|
const isOk = ((res.status / 200) | 0) === 1; |
||||
|
return isOk ? result : result.then(Promise.reject); |
||||
|
}) |
||||
|
.then(text => { |
||||
|
const parsable = text && isString(text); |
||||
|
return opts.contentTypeJson && parsable ? JSON.parse(text) : text; |
||||
|
}); |
||||
|
} |
||||
|
|
||||
/** |
__props(opts = {}, data) { |
||||
* Execute remote request |
const typeJson = opts.contentTypeJson; |
||||
* @param {string} url Url |
const headers = opts.headers || {}; |
||||
* @param {Object} [opts={}] Options |
const fetchOpts = opts.fetchOptions || {}; |
||||
* @param {Function} [clb=null] Callback |
|
||||
* @param {Function} [clbErr=null] Error callback |
|
||||
* @private |
|
||||
*/ |
|
||||
request(url, opts = {}, clb = null, clbErr = null) { |
|
||||
const typeJson = this.get('contentTypeJson'); |
|
||||
const headers = this.get('headers') || {}; |
|
||||
const params = this.get('params'); |
|
||||
const reqHead = 'X-Requested-With'; |
const reqHead = 'X-Requested-With'; |
||||
const typeHead = 'Content-Type'; |
const typeHead = 'Content-Type'; |
||||
const bodyObj = opts.body || {}; |
|
||||
let fetchOptions; |
|
||||
let body; |
let body; |
||||
|
|
||||
for (let param in params) { |
|
||||
bodyObj[param] = params[param]; |
|
||||
} |
|
||||
|
|
||||
if (isUndefined(headers[reqHead])) { |
if (isUndefined(headers[reqHead])) { |
||||
headers[reqHead] = 'XMLHttpRequest'; |
headers[reqHead] = 'XMLHttpRequest'; |
||||
} |
} |
||||
|
|
||||
// With `fetch`, have to send FormData without any 'Content-Type'
|
|
||||
// https://stackoverflow.com/questions/39280438/fetch-missing-boundary-in-multipart-form-data-post
|
|
||||
|
|
||||
if (isUndefined(headers[typeHead]) && typeJson) { |
if (isUndefined(headers[typeHead]) && typeJson) { |
||||
headers[typeHead] = 'application/json; charset=utf-8'; |
headers[typeHead] = 'application/json; charset=utf-8'; |
||||
} |
} |
||||
|
|
||||
if (typeJson) { |
if (data) { |
||||
body = JSON.stringify(bodyObj); |
if (typeJson) { |
||||
} else { |
body = JSON.stringify(data); |
||||
body = new FormData(); |
} else { |
||||
|
body = new FormData(); |
||||
|
|
||||
for (let bodyKey in bodyObj) { |
for (let key in data) { |
||||
body.append(bodyKey, bodyObj[bodyKey]); |
body.append(key, data[key]); |
||||
|
} |
||||
} |
} |
||||
} |
} |
||||
fetchOptions = { |
|
||||
method: opts.method || 'post', |
|
||||
credentials: this.get('credentials'), |
|
||||
headers |
|
||||
}; |
|
||||
|
|
||||
// Body should only be included on POST method
|
const result = { |
||||
if (fetchOptions.method === 'post') { |
method: body ? 'POST' : 'GET', |
||||
fetchOptions.body = body; |
credentials: opts.credentials, |
||||
} |
headers, |
||||
|
body, |
||||
const fetchOpts = this.get('fetchOptions') || {}; |
}; |
||||
const addOpts = isFunction(fetchOpts) |
|
||||
? fetchOpts(fetchOptions) |
|
||||
: fetchOptions; |
|
||||
|
|
||||
this.onStart(); |
return { |
||||
this.fetch(url, { |
...result, |
||||
...fetchOptions, |
...(isFunction(fetchOpts) ? fetchOpts(result) : fetchOpts), |
||||
...(addOpts || {}) |
}; |
||||
}) |
|
||||
.then(res => |
|
||||
((res.status / 200) | 0) == 1 |
|
||||
? res.text() |
|
||||
: res.text().then(text => Promise.reject(text)) |
|
||||
) |
|
||||
.then(text => this.onResponse(text, clb)) |
|
||||
.catch(err => this.onError(err, clbErr)); |
|
||||
} |
} |
||||
}); |
} |
||||
|
|||||
Loading…
Reference in new issue