/** * Return cookie with provided key */ function getCookie(key) { if(document.cookie && document.cookie !== '') { const cookie = document.cookie.split(';') .find(c => c.trim().startsWith(key + '=')) return cookie ? decodeURIComponent(cookie.split('=')[1]) : null; } return null; } /** * CSRF token provided by Django */ var csrfToken = null; /** * Get CSRF token */ export function getCsrf() { if(csrfToken === null) csrfToken = getCookie('csrftoken') return csrfToken; } // TODO: prevent duplicate simple fetch /** * Provide interface used to fetch and manipulate objects. */ export default class Model { /** * Instanciate model with provided data and options. * By default `url` is taken from `data.url_`. */ constructor(data={}, {url=null, ...options}={}) { this.url = url || data.url_; this.options = options; this.commit(data); } get errors() { return this.data && this.data.__errors__ } /** * Get instance id from its data */ static getId(data) { return 'id' in data ? data.id : data.pk; } /** * Return fetch options */ static getOptions(options) { return { headers: { 'Content-Type': 'application/json', 'Accept': 'application/json', 'X-CSRFToken': getCsrf(), }, ...options, } } /** * Return model instances for the provided list of model data. * @param {Array} items: array of data * @param {Object} options: options passed down to all model instances */ static fromList(items, options={}) { return items ? items.map(d => new this(d, options)) : [] } /** * Fetch item from server */ static fetch(url, {many=false, ...options}={}, args={}) { options = this.getOptions(options) const request = fetch(url, options).then(response => response.json()); if(many) return request.then(data => { if(!(data instanceof Array)) data = data.results return this.fromList(data, args) }) else return request.then(data => new this(data, {url: url, ...args})); } /** * Fetch data from server. */ fetch(options) { options = this.constructor.getOptions(options) return fetch(this.url, options) .then(response => response.json()) .then(data => this.commit(data)); } /** * Call API action on object. */ action(path, options, commit=false) { options = this.constructor.getOptions(options) const promise = fetch(this.url + path, options); return commit ? promise.then(data => data.json()) .then(data => { this.commit(data); this.data }) : promise; } /** * Update instance's data with provided data. Return None */ commit(data) { this.data = data; this.id = this.constructor.getId(this.data); } /** * Update model data, without reset previous value */ update(data) { this.data = {...this.data, ...data} this.id = this.constructor.getId(this.data) } /** * Save instance into localStorage. */ store(key) { window.localStorage.setItem(key, JSON.stringify(this.data)); } /** * Load model instance from localStorage. */ static storeLoad(key) { let item = window.localStorage.getItem(key); return item === null ? item : new this(JSON.parse(item)); } /** * Return true if model instance has no data */ get isEmpty() { return !this.data || Object.keys(this.data).findIndex(k => !!this.data[k] && this.data[k] !== 0) == -1 } /** * Return error for a specific attribute name if any */ error(attr=null) { return attr === null ? this.errors : this.errors && this.errors[attr] } } /** * List of models */ export class Set { constructor(model, {items=[],url=null,args={},unique=null,max=null,storeKey=null}={}) { this.items = []; this.model = model; this.url = url; this.unique = unique; this.max = max; this.storeKey = storeKey; for(var item of items) this.push(item, {args: args, save: false}); } get length() { return this.items.length } /** * Fetch multiple items from server */ static fetch(model, url, options=null, args=null) { options = model.getOptions(options) return fetch(url, options) .then(response => response.json()) .then(data => (data instanceof Array ? data : data.results) .map(d => new model(d, {url: url, ...args}))) } /** * Load list from localStorage */ static storeLoad(model, key, args={}) { let items = window.localStorage.getItem(key); return new this(model, {...args, storeKey: key, items: items ? JSON.parse(items) : []}); } /** * Store list into localStorage */ store() { this.storeKey && window.localStorage.setItem(this.storeKey, JSON.stringify( this.items.map(i => i.data))); } /** * Save item */ save() { this.storeKey && this.store(); } /** * Get item at index */ get(index) { return this.items[index] } /** * Find an item by id or using a predicate function */ find(pred) { return pred instanceof Function ? this.items.find(pred) : this.items.find(x => x.id == pred.id); } /** * Find item index by id or using a predicate function */ findIndex(pred) { return pred instanceof Function ? this.items.findIndex(pred) : this.items.findIndex(x => x.id == pred.id); } /** * Add item to set, return index. */ push(item, {args={},save=true}={}) { item = item instanceof this.model ? item : new this.model(item, args); if(this.unique) { let index = this.findIndex(item); if(index > -1) return index; } if(this.max && this.items.length >= this.max) this.items.splice(0,this.items.length-this.max) this.items.push(item); save && this.save(); return this.items.length-1; } /** * Remove item from set by index */ remove(index, {save=true}={}) { this.items.splice(index,1); save && this.save(); } /** * Clear items, assign new ones */ reset(items=[]) { // TODO: check reactivity this.items = [] for(var item of items) this.push(item) } move(from, to) { if(from >= this.length || to > this.length) throw "source or target index is not in range" const value = this.items[from] this.items.splice(from, 1) this.items.splice(to, 0, value) } } Set[Symbol.iterator] = function () { return this.items[Symbol.iterator](); }