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 { |
|||
// Prefix identifier that will be used inside storing and loading
|
|||
// @deprecated
|
|||
id: 'gjs-', |
|||
|
|||
// Default storage type. Available: local | remote
|
|||
type: 'local', |
|||
|
|||
// Enable/Disable autosaving
|
|||
autosave: true, |
|||
|
|||
// Indicates if load data inside editor after init
|
|||
// Enable/Disable autoload of data on editor init
|
|||
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)
|
|||
// need to be done before save. Useful with remoteStorage to reduce remote calls
|
|||
stepsBeforeSave: 1, |
|||
|
|||
//Enable/Disable components model (JSON format)
|
|||
storeComponents: true, |
|||
|
|||
//Enable/Disable styles model (JSON format)
|
|||
storeStyles: true, |
|||
|
|||
//Enable/Disable saving HTML template
|
|||
storeHtml: true, |
|||
|
|||
//Enable/Disable saving CSS template
|
|||
storeCss: true, |
|||
|
|||
// ONLY FOR LOCAL STORAGE
|
|||
// If enabled, checks if browser supports Local Storage
|
|||
checkLocal: true, |
|||
|
|||
// ONLY FOR REMOTE STORAGE
|
|||
// Custom parameters to pass with the remote storage request, eg. csrf token
|
|||
params: {}, |
|||
|
|||
// Custom headers for the remote storage request
|
|||
headers: {}, |
|||
|
|||
// Endpoint where to save all stuff
|
|||
urlStore: '', |
|||
|
|||
// Endpoint where to fetch data
|
|||
urlLoad: '', |
|||
|
|||
//Callback before request
|
|||
beforeSend(jqXHR, settings) {}, |
|||
|
|||
//Callback after request
|
|||
onComplete(jqXHR, status) {}, |
|||
|
|||
// set contentType paramater of $.ajax
|
|||
// true: application/json; charset=utf-8'
|
|||
// false: 'x-www-form-urlencoded'
|
|||
contentTypeJson: true, |
|||
|
|||
credentials: 'include', |
|||
|
|||
// Pass custom options to fetch API (remote storage)
|
|||
// You can pass a simple object: { someOption: 'someValue' }
|
|||
// or a function which returns and object to add:
|
|||
// currentOpts => {
|
|||
// return currentOpts.method === 'post' ? { method: 'patch' } : {};
|
|||
// }
|
|||
fetchOptions: '', |
|||
/** |
|||
* Callback triggered before the store call (can be asynchronous). |
|||
* This can be used to enrich the project data to store. |
|||
*/ |
|||
onStore: data => data, |
|||
|
|||
/** |
|||
* Callback triggered after the load call (can be asynchronous). |
|||
*/ |
|||
onLoad: data => data, |
|||
|
|||
// Default storage options
|
|||
options: { |
|||
local: { |
|||
key: 'gjsProject', |
|||
|
|||
// If enabled, checks if browser supports LocalStorage
|
|||
checkLocal: true, |
|||
}, |
|||
remote: { |
|||
// Custom headers
|
|||
headers: {}, |
|||
|
|||
// Endpoint URL where to store data project
|
|||
urlStore: '', |
|||
|
|||
// Endpoint URL where to load data project
|
|||
urlLoad: '', |
|||
|
|||
// set contentType paramater of $.ajax
|
|||
// true: application/json; charset=utf-8'
|
|||
// false: 'x-www-form-urlencoded'
|
|||
contentTypeJson: true, |
|||
|
|||
// Pass custom options to fetch API (remote storage)
|
|||
// You can pass a simple object: { someOption: 'someValue' }
|
|||
// or a function which returns and object to add:
|
|||
// currentOpts => {
|
|||
// return currentOpts.method === 'POST' ? { method: 'PATCH' } : {};
|
|||
// }
|
|||
fetchOptions: '', |
|||
|
|||
credentials: 'include', |
|||
|
|||
/** |
|||
* The remote storage sends the project data as a body of the request. |
|||
* You can use this method to update the body before the store call in order to align |
|||
* with your API requirements. |
|||
*/ |
|||
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'; |
|||
|
|||
export default Model.extend({ |
|||
defaults: { |
|||
checkLocal: true |
|||
}, |
|||
|
|||
/** |
|||
* @private |
|||
*/ |
|||
store(data, clb = () => {}) { |
|||
if (this.hasLocal()) { |
|||
for (let key in data) localStorage.setItem(key, data[key]); |
|||
export default class LocalStorage { |
|||
async store(data, opts = {}) { |
|||
if (this.hasLocal(opts, true)) { |
|||
localStorage.setItem(opts.key, JSON.stringify(data)); |
|||
} |
|||
} |
|||
|
|||
clb && clb(); |
|||
}, |
|||
|
|||
/** |
|||
* @private |
|||
*/ |
|||
load(keys, clb = () => {}) { |
|||
const result = {}; |
|||
async load(opts = {}) { |
|||
let result = {}; |
|||
|
|||
if (this.hasLocal()) { |
|||
for (let i = 0, len = keys.length; i < len; i++) { |
|||
const value = localStorage.getItem(keys[i]); |
|||
if (value) result[keys[i]] = value; |
|||
} |
|||
if (this.hasLocal(opts, true)) { |
|||
result = JSON.parse(localStorage.getItem(opts.key) || '{}'); |
|||
} |
|||
|
|||
clb && clb(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)) { |
|||
win && console.warn("Your browser doesn't support localStorage"); |
|||
hasLocal(opts = {}, thr) { |
|||
if (opts.checkLocal && (!hasWin() || !localStorage)) { |
|||
if (thr) throw new Error('localStorage not available'); |
|||
return false; |
|||
} |
|||
|
|||
return true; |
|||
} |
|||
}); |
|||
} |
|||
|
|||
@ -1,146 +1,66 @@ |
|||
import Backbone from 'backbone'; |
|||
import fetch from 'utils/fetch'; |
|||
import { isUndefined, isFunction } from 'underscore'; |
|||
import { isUndefined, isFunction, isString } from 'underscore'; |
|||
|
|||
export default Backbone.Model.extend({ |
|||
fetch, |
|||
|
|||
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]; |
|||
} |
|||
export default class RemoteStorage { |
|||
async store(data, opts = {}) { |
|||
await this.request(opts.urlStore, this.__props(opts, data), opts); |
|||
} |
|||
|
|||
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) { |
|||
this.request(this.get('urlLoad'), { method: 'get' }, clb, clbErr); |
|||
}, |
|||
request(url, props = {}, opts = {}) { |
|||
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; |
|||
}); |
|||
} |
|||
|
|||
/** |
|||
* Execute remote request |
|||
* @param {string} url Url |
|||
* @param {Object} [opts={}] Options |
|||
* @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'); |
|||
__props(opts = {}, data) { |
|||
const typeJson = opts.contentTypeJson; |
|||
const headers = opts.headers || {}; |
|||
const fetchOpts = opts.fetchOptions || {}; |
|||
const reqHead = 'X-Requested-With'; |
|||
const typeHead = 'Content-Type'; |
|||
const bodyObj = opts.body || {}; |
|||
let fetchOptions; |
|||
let body; |
|||
|
|||
for (let param in params) { |
|||
bodyObj[param] = params[param]; |
|||
} |
|||
|
|||
if (isUndefined(headers[reqHead])) { |
|||
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) { |
|||
headers[typeHead] = 'application/json; charset=utf-8'; |
|||
} |
|||
|
|||
if (typeJson) { |
|||
body = JSON.stringify(bodyObj); |
|||
} else { |
|||
body = new FormData(); |
|||
if (data) { |
|||
if (typeJson) { |
|||
body = JSON.stringify(data); |
|||
} else { |
|||
body = new FormData(); |
|||
|
|||
for (let bodyKey in bodyObj) { |
|||
body.append(bodyKey, bodyObj[bodyKey]); |
|||
for (let key in data) { |
|||
body.append(key, data[key]); |
|||
} |
|||
} |
|||
} |
|||
fetchOptions = { |
|||
method: opts.method || 'post', |
|||
credentials: this.get('credentials'), |
|||
headers |
|||
}; |
|||
|
|||
// Body should only be included on POST method
|
|||
if (fetchOptions.method === 'post') { |
|||
fetchOptions.body = body; |
|||
} |
|||
|
|||
const fetchOpts = this.get('fetchOptions') || {}; |
|||
const addOpts = isFunction(fetchOpts) |
|||
? fetchOpts(fetchOptions) |
|||
: fetchOptions; |
|||
const result = { |
|||
method: body ? 'POST' : 'GET', |
|||
credentials: opts.credentials, |
|||
headers, |
|||
body, |
|||
}; |
|||
|
|||
this.onStart(); |
|||
this.fetch(url, { |
|||
...fetchOptions, |
|||
...(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)); |
|||
return { |
|||
...result, |
|||
...(isFunction(fetchOpts) ? fetchOpts(result) : fetchOpts), |
|||
}; |
|||
} |
|||
}); |
|||
} |
|||
|
|||
Loading…
Reference in new issue