coral-spectrum/coral-messenger/src/scripts/Messenger.js
/**
* Copyright 2021 Adobe. All rights reserved.
* This file is licensed to you under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. You may obtain a copy
* of the License at http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under
* the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
* OF ANY KIND, either express or implied. See the License for the specific language
* governing permissions and limitations under the License.
*/
import {commons} from '../../../coral-utils';
const SCOPE_SELECTOR = ':scope > ';
/**
* Messenger will used to pass the messages from child component to its parent. Currently we are relying on
* events to do the job. When a large DOM is connected, these events as a bulk leads to delays.
* With the use of messenger we will directly call the parent method provided in the observed messages list.
* The current implmentation only supports one to many mapping i.e. one parent and many children and any
* in child property will result in execution of only one parent method. This should be used purely for
* coral internal events and not any DOM based or public events.
*
* Limitations :
* - This doesnot support the case where any change in child property, needs to be notified
* to two or more parents. This is achievable, but not currently supported.
* - Use this to post message only coral internal events.
* - Do not use for DOM events or public events.
* @private
*/
class Messenger {
/** @ignore */
constructor(element) {
this._element = element;
this._connected = false;
this._clearQueue();
this._clearListeners();
}
/**
* checks whether Messenger is connected or not.
* @returns {Boolean} true if connected
* @private
*/
get isConnected() {
return this._connected === true;
}
/**
* checks whether the event is silenced or not
* @returns {Boolean} true if silenced
* @private
*/
get isSilenced() {
return this._element._silenced === true;
}
/**
* specifies the list of listener attached to messenger.
* @returns {Array} array of listeners
* @private
*/
get listeners() {
return this._listeners;
}
/**
* add a message to the queue only if messenger is not connected
* message will be added only if element is not connected.
* @private
*/
_addMessageToQueue(message, detail) {
if(!this.isConnected) {
this._queue.push({
message: message,
detail: detail
});
}
}
/**
* executes the stored queue messages.
* It will be executed when element is connected.
* @private
*/
_executeQueue() {
this._queue.forEach((options) => {
this._postMessage(options.message, options.detail);
});
this._clearQueue();
}
/**
* empty the stored queue message
* @private
*/
_clearQueue() {
this._queue = [];
}
/**
* clears the listeners
* @private
*/
_clearListeners() {
this._listeners = [];
}
/**
* element should call this method when they are connected in DOM.
* its the responsibility of the element to call this hook
* @triggers `${element.tagName.toLowerCase()}:_messengerconnected`
* @private
*/
connect() {
if(!this.isConnected) {
let element = this._element;
this._connected = true;
element.trigger(`${element.tagName.toLowerCase()}:_messengerconnected`, {
handler : this.registerListener.bind(this)
});
// post all stored messages
this._executeQueue();
}
}
/**
* add the listener to messenger
* this handler will be passed when messengerconnect event is trigger
* the handler needs to be executed by listeners.
* @private
*/
registerListener(listener) {
if(listener) {
this._listeners.push(listener);
}
}
/**
* post the provided message to all listener.
* @param {String} message which should be posted
* @param {Object} additional detail which needs to be posted.
* @private
*/
_postMessage(message, detail) {
let element = this._element;
this.listeners.forEach((listener) => {
let observedMessages = listener.observedMessages;
let messageInfo = observedMessages[message];
if(messageInfo) {
let selector;
let handler;
if(typeof messageInfo === 'string') {
selector = "*";
handler = messageInfo;
} else if(typeof messageInfo === 'object') {
selector = messageInfo.selector || "*";
handler = messageInfo.handler;
}
if(selector.indexOf(SCOPE_SELECTOR) === 0 ) {
if(!listener.id) {
listener.id = commons.getUID();
}
selector = selector.replace(SCOPE_SELECTOR, `#${listener.id} > `);
}
if(element.matches(selector)) {
listener[handler].call(listener, new Event({
target: element,
detail: detail,
type: message,
currentTarget: listener
}));
}
}
});
}
/**
* post the provided message to all listener,
* along with validating silencing and storing in queue
* @param {String} message which should be posted
* @param {Object} additional detail which needs to be posted.
* @private
*/
postMessage(message, detail) {
if(this.isSilenced) {
return;
}
if(!this.isConnected) {
this._addMessageToQueue(message, detail);
return;
}
// element got disconnect and messenger not notified.
if(!this._element.isConnected) {
// disconnect messenger and again post the same message,
// message will get store in queue.
this.disconnect();
this.postMessage(message, detail);
return;
}
this._postMessage(message, detail);
}
/**
* element should call this method when they are disconnected from DOM.
* Its the responsibility of the element to call this hook
* @private
*/
disconnect() {
if(this.isConnected) {
this._connected = false;
this._clearListeners();
this._clearQueue();
}
}
}
/**
* This Event class is just a bogus class, current message callback aspects
* actual event as a parameter, since we are directly calling the method instead
* of triggering event, will pass an instance of this disguised object,
* to avoid breaks.
* This just disguise the most used functionality of event object
* @private
*/
class Event {
constructor(options) {
this._detail = options.detail;
this._target = options.target;
this._type = options.type;
this._currentTarget = options.currentTarget;
this._defaultPrevented = false;
this._propagationStopped = false;
this._immediatePropagationStopped = false;
}
get detail() {
return this._detail;
}
get type() {
return this._type;
}
get target() {
return this._target;
}
get currentTarget() {
return this._currentTarget;
}
preventDefault() {
this._defaultPrevented = true;
}
stopPropagation() {
this._propagationStopped = true;
}
stopImmediatePropagation() {
this._immediatePropagationStopped = true;
}
}
export default Messenger;