import deliverEvent from './event-handler'
import Event from './event'
import loadPlugins from './plugin-loader'
import observeEvents from './event-observers'
import Plugin from './plugin'
import Store from './store'
const listeners = new WeakMap()
const stores = new WeakMap()
/**
* Invoked when notifying a handler about an event it has registered for.
*
* @callback eventCallback
* @param {Event} Event object associated with the registered event.
* @since 0.0.0
*/
/**
* Add one or more items to the system. This event is triggered before the item(s)
* is/are actually persisted. If the event is cancelled, the item(s) will not
* be persisted. Furthermore, the item(s) and their associated data can be
* influenced by various event handlers.
*
* @event add
* @type {Event}
* @property {Event#payload} payload - Any data to store with the item.
* In addition to the standardized properties described below, you may
* include other custom properties that are useful to your specific use case
* or plug-in as well. Note that the payload may be an Object or an Array.
* In order to group multiple entries to be added, include an Array as the payload.
* In that case, each array element will represent a single item and its associated
* data. If you would like to group multiple items this way, the payload properties
* documented here apply to each object in the array.
* @property {*} payload.item - The item to be added. May be anything,
* such as a File, Blob, <canvas>, etc.
* @property {string} [payload.id] - A unique ID for this item. Will be
* calculated by Core if not provided.
* @property {string} [payload.name] - A name for the item.
* @since 0.0.0
* @example
* // add a single item, ID will be generated
* api.fire(new Event({
* type: 'add',
* payload: {
* item: blob1,
* name: 'cat photos'
* }
* }))
*
* // add multiple items, IDs will be generated
* api.fire(new Event({
* type: 'add',
* payload: [
* {
* item: blob1,
* name: 'cat photos'
* },
* {
* item: blob2,
* name: 'dog photos'
* }
* ]
* }))
*/
/**
* Indicates that one or more items have been added to the system.
* This is an informational event.
*
* @event added
* @type {Event}
* @property {Event#payload} payload - This will always be an array of IDs
* representing the added items.
* @since 0.0.0
*/
/**
* Indicates that all plugins have been successfully loaded by the library.
* This is an informational event.
*
* @event allPluginsLoaded
* @type {Event}
* @since 0.0.0
*/
/**
* Removes one or more items from the system. This event is triggered before the item(s)
* is/are actually persisted. If the event is cancelled, the item(s) will not
* be removed. To remove only a subset of the items originally bound for removal,
* event handlers can return the subset of item IDs to remove. This means that another way
* to prevent all items from being removed is to return an empty array.
*
* @event remove
* @type {Event}
* @property {Event#payload} payload - One or more items to remove from the system. To remove
* a single item, simply include that item's ID as the payload. To remove multiple items,
* pass an array of the item IDs as the payload.
* @since 0.0.0
* @example
* // remove a single item w/ an ID of 12345
* api.fire(new Event({
* type: 'remove',
* payload: 12345
* }))
*
* // another way to remove a single item w/ an ID of 12345
* api.fire(new Event({
* type: 'remove',
* payload: [12345]
* }))
*
* // remove two items w/ an IDs of 12345 and 67890 respectively
* api.fire(new Event({
* type: 'remove',
* payload: [12345, 67890]
* }))
*/
/**
* Indicates that one or more items have been removed from the system.
* This is an informational event.
*
* @event removed
* @type {Event}
* @property {Event#payload} payload - IDs of the removed items. This will always be an array.
* @since 0.0.0
*/
/**
* Request that the system be reset. Currently, this only involves deleting the store of items.
*
* @event reset
* @type {Event}
* @since 0.0.0
* @example
* // reset the system
* api.fire(new Event({
* type: 'reset'
* }))
*/
/**
* Indicates that the system has been reset.
* This is an informational event.
*
* @event resetComplete
* @type {Event}
* @since 0.0.0
*/
/**
* Requests that a specific item be updated. The properties included in the event payload
* will replace any existing properties on the matching item. If specified properties do not yet
* exist, they will be added. The item to update must be specified with an `id` property
* in the event payload.
*
* @event update
* @type {Event}
* @property {Event#payload} payload - The properties to update on the target item.
* @property {(string|number)} payload.id - The ID of the item to update.
* @since 0.0.0
* @example
* // change the name of an item w/ and ID of 123
* api.fire(new Event({
* type: 'update',
* payload: {
* id: 123,
* name: 'this is my new name'
* }
* }))
*/
/**
* Indicates that a specific item has been updated. The payload describes the event that has
* been updated (via the ID property) along with the updated properties.
*
* @event updated
* @type {Event}
* @property {Event#payload} payload - Describes changes made to a specific item.
* @property {(string|number)} payload.id - The ID of the item that was updated.
* @since 0.0.0
*/
/**
* Main class for core plug-in.
*
* @extends Plugin
* @listens add
* @listens remove
* @listens reset
* @listens update
* @fires allPluginsLoaded
* @fires added
* @fires removed
* @fires resetComplete
* @fires updated
* @since 0.0.0
* @example
* import Core from 'core'
*
* const modernUploader = new Core([
* new SomePlugin1(),
* new SomePlugin2()
* ])
*/
class Core extends Plugin {
/**
* Loads an array of plug-ins.
*
* @param {Array} plugins - Plugin object instances.
* @throws {Error} If one of the passed plug-ins does not extend Plugin or if no plug-ins are passed.
* @since 0.0.0
*/
constructor(plugins = []) {
super('core')
if (!window.Promise) {
throw new Error('Promises are required by this library, but are supported in this browser. Please use a Promise polyfill.')
}
listeners.set(this, {})
stores.set(this, new Store())
observeEvents(this, stores.get(this))
loadPlugins(plugins, this)
}
/**
* This looks up items in the store. It also provides an opportunity to
* retrieve all items maintained by the system. Currently, items may be
* looked up by ID, or you may retrieve all items from the store. By
* default, the returned items will be deeply cloned, but this can be
* switched off to save processor cycles for large data sets.
* Any records that you would like to modify must be sent back
* to the system in an [updateData event]{@link event:updateData}.
*
* @param {(Object|undefined)} entryQuery - Retrieve the associated
* records from the store. An object describes the specific entries to return.
* If this is undefined, all records will be returned.
* @param {(string|number|Array)} entryQuery.id - The ID or IDs of the entries
* to retrieve.
* @param {boolean} [clone=true]
* @returns {(Object|Array|null)} One or more matching records, or null if no matches were found.
* If only one record was requested, the an Array will not be returned - only the
* entry object or null if the entry cannot be located. If multiple records are
* requested, an array will always be returned with any matching entries.
* If no entries match, the array will be empty.
* @since 0.0.0
* @example
* // get one record
* const record = api.get({id: 'uuid-000'})
* expect(record).toEqual(uuid000Record)
*
* // get multiple records
* const record = api.get({id: ['uuid-000', 'uuid-001']})
* expect(record).toEqual([uuid000Record, uuid001Record])
*
* // get all records
* const record = api.get()
* expect(record).toEqual([uuid000Record, uuid001Record, uuid002Record, ...])
*/
get(entryQuery, clone = true) {
const myStore = stores.get(this)
if (!entryQuery) {
return myStore.getAll(clone)
}
if (typeof entryQuery !== 'object') {
throw new Error('entryQuery parameter must be an object!')
}
if (!entryQuery.id) {
throw new Error('You may only query for IDs at the moment - so your entryQuery object must include an "id" property.')
}
if (!Array.isArray(entryQuery.id)) {
return myStore.getById(entryQuery.id, clone)
}
const matchingRecords = []
const ids = [].concat(entryQuery.id)
ids.forEach(id => {
const matchingRecord = myStore.getById(id, clone)
matchingRecord && matchingRecords.push(matchingRecord)
})
return matchingRecords
}
/**
* Trigger an event to be passed to all other registered event listeners. This event will be
* bubbled, starting with the first registered listener, and ending with the last.
*
* @param {Event} event - event to pass to all registered listeners for this event type.
* @returns {Promise} Once the event has been bubbled to all registered listeners,
* the result will be returned by resolving or rejecting this returned Promise, depending on the outcome.
* If the event is cancelled by a listener, this Promise will be rejected with any data provided
* by the cancelling listener. If this event is not cancelled, any data provided by all listeners
* will be included when resolving the returned Promise.
* @since 0.0.0
* @example
* // A plug-in that wants to add a new item (File/Blob/etc),
* // will fire an "add" event, which will be handled by the
* // core plug-in, resulting in the addition of the item
* // to the internal cache maintained by the core plug-in.
* // But some other plug-in could also prevent this add
* // from happening before it reaches the core plug-in
* // (perhaps a validator plug-in, for example).
* core.fire(
* new Event({
* type: 'add',
* payload: {
* item: someFile
* }
* })
* )
* .then(
* function added(event) {
* // the item was added
* },
*
* function notAdded(event) {
* // something prevented item from being added
* }
* )
*/
fire(event) {
if ( !(event instanceof Event) ) {
throw new Error('You must pass an Event object to the fire method!')
}
const myListeners = listeners.get(this)
return deliverEvent(event, myListeners)
}
/**
* Register one or more handlers for one or more specific events.
*
* @param {(string|Object)} typeOrListenersObject - Register one listener by specifying the event type as a string here,
* followed by the listener function as the next parameter, or pass a single object parameter
* with event type properties and listener function values to conveniently register for multiple events.
* @param {eventCallback} listener - Listener Function to be called when the passed event type is fired.
* This parameter is ignored if the first parameter is a listener object. In that case, it must be
* supplied as the value for each event type property in the passed object.
* @since 0.0.0
* @example
* // register a single listener for an "add" event
* core.on('add', event => {
* // handle "add" event
* })
*
* // register a listener for the "add" event,
* // and another for the "remove" event
* core.on({
* add: event => {
* // handle "add" event
* },
* remove: event => {
* // handle "remove" event
* }
* })
*/
on(typeOrListenersObject, listener) {
if (typeof typeOrListenersObject !== 'object' && typeof typeOrListenersObject !== 'string') {
throw new Error('The first argument to the on method must be a string or object!')
}
if (typeof listener !== 'function') {
throw new Error('You must pass a callback function as the second argument to the on method!')
}
if (typeof typeOrListenersObject === 'string') {
const type = typeOrListenersObject
const myListeners = listeners.get(this)
const typeListeners = myListeners[type] || []
typeListeners.push(listener)
myListeners[type] = typeListeners
listeners.set(this, myListeners)
}
else {
Object.keys(typeOrListenersObject).forEach(type => {
this.on(type, typeOrListenersObject[type])
})
}
}
/**
* Register a listener for all events. This may be useful for plug-ins that need to be notified
* whenever any event is triggered in the system. A good example would be a plug-in that logs all
* activity.
*
* @param {eventCallback} listener - Listener function
* @since 0.0.0
* @example
* // register for all system events
* core.onAll(event => {
* console.log(`I just received an event of type ${event.type}.`)
* // do something else with this event:
* // - return a new value for the originator
* // - cancel the event
* // - passively listen or react in some other way
* })
*/
onAll(listener) {
if (typeof listener !== 'function') {
throw new Error('You must pass a callback function to the onAll method!')
}
const myListeners = listeners.get(this)
Object.keys(myListeners).concat('*').forEach(type => this.on(type, listener))
}
}
export default Core