ExamplesPlaygroundReference Source

coral-spectrum/coral-messenger/src/scripts/Messenger.js

  1. /**
  2. * Copyright 2021 Adobe. All rights reserved.
  3. * This file is licensed to you under the Apache License, Version 2.0 (the "License");
  4. * you may not use this file except in compliance with the License. You may obtain a copy
  5. * of the License at http://www.apache.org/licenses/LICENSE-2.0
  6. *
  7. * Unless required by applicable law or agreed to in writing, software distributed under
  8. * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
  9. * OF ANY KIND, either express or implied. See the License for the specific language
  10. * governing permissions and limitations under the License.
  11. */
  12.  
  13. import {commons} from '../../../coral-utils';
  14.  
  15. const SCOPE_SELECTOR = ':scope > ';
  16.  
  17. /**
  18. * Messenger will used to pass the messages from child component to its parent. Currently we are relying on
  19. * events to do the job. When a large DOM is connected, these events as a bulk leads to delays.
  20. * With the use of messenger we will directly call the parent method provided in the observed messages list.
  21. * The current implmentation only supports one to many mapping i.e. one parent and many children and any
  22. * in child property will result in execution of only one parent method. This should be used purely for
  23. * coral internal events and not any DOM based or public events.
  24. *
  25. * Limitations :
  26. * - This doesnot support the case where any change in child property, needs to be notified
  27. * to two or more parents. This is achievable, but not currently supported.
  28. * - Use this to post message only coral internal events.
  29. * - Do not use for DOM events or public events.
  30. * @private
  31. */
  32.  
  33. class Messenger {
  34. /** @ignore */
  35. constructor(element) {
  36. this._element = element;
  37. this._connected = false;
  38. this._clearQueue();
  39. this._clearListeners();
  40. }
  41.  
  42. /**
  43. * checks whether Messenger is connected or not.
  44. * @returns {Boolean} true if connected
  45. * @private
  46. */
  47. get isConnected() {
  48. return this._connected === true;
  49. }
  50.  
  51. /**
  52. * checks whether the event is silenced or not
  53. * @returns {Boolean} true if silenced
  54. * @private
  55. */
  56. get isSilenced() {
  57. return this._element._silenced === true;
  58. }
  59.  
  60. /**
  61. * specifies the list of listener attached to messenger.
  62. * @returns {Array} array of listeners
  63. * @private
  64. */
  65. get listeners() {
  66. return this._listeners;
  67. }
  68.  
  69. /**
  70. * add a message to the queue only if messenger is not connected
  71. * message will be added only if element is not connected.
  72. * @private
  73. */
  74. _addMessageToQueue(message, detail) {
  75. if(!this.isConnected) {
  76. this._queue.push({
  77. message: message,
  78. detail: detail
  79. });
  80. }
  81. }
  82.  
  83. /**
  84. * executes the stored queue messages.
  85. * It will be executed when element is connected.
  86. * @private
  87. */
  88. _executeQueue() {
  89. this._queue.forEach((options) => {
  90. this._postMessage(options.message, options.detail);
  91. });
  92. this._clearQueue();
  93. }
  94.  
  95. /**
  96. * empty the stored queue message
  97. * @private
  98. */
  99. _clearQueue() {
  100. this._queue = [];
  101. }
  102.  
  103. /**
  104. * clears the listeners
  105. * @private
  106. */
  107. _clearListeners() {
  108. this._listeners = [];
  109. }
  110.  
  111. /**
  112. * element should call this method when they are connected in DOM.
  113. * its the responsibility of the element to call this hook
  114. * @triggers `${element.tagName.toLowerCase()}:_messengerconnected`
  115. * @private
  116. */
  117. connect() {
  118. if(!this.isConnected) {
  119. let element = this._element;
  120.  
  121. this._connected = true;
  122.  
  123. element.trigger(`${element.tagName.toLowerCase()}:_messengerconnected`, {
  124. handler : this.registerListener.bind(this)
  125. });
  126. // post all stored messages
  127. this._executeQueue();
  128. }
  129. }
  130.  
  131. /**
  132. * add the listener to messenger
  133. * this handler will be passed when messengerconnect event is trigger
  134. * the handler needs to be executed by listeners.
  135. * @private
  136. */
  137. registerListener(listener) {
  138. if(listener) {
  139. this._listeners.push(listener);
  140. }
  141. }
  142.  
  143. /**
  144. * post the provided message to all listener.
  145. * @param {String} message which should be posted
  146. * @param {Object} additional detail which needs to be posted.
  147. * @private
  148. */
  149. _postMessage(message, detail) {
  150. let element = this._element;
  151. this.listeners.forEach((listener) => {
  152. let observedMessages = listener.observedMessages;
  153. let messageInfo = observedMessages[message];
  154.  
  155. if(messageInfo) {
  156. let selector;
  157. let handler;
  158. if(typeof messageInfo === 'string') {
  159. selector = "*";
  160. handler = messageInfo;
  161. } else if(typeof messageInfo === 'object') {
  162. selector = messageInfo.selector || "*";
  163. handler = messageInfo.handler;
  164. }
  165.  
  166. if(selector.indexOf(SCOPE_SELECTOR) === 0 ) {
  167. if(!listener.id) {
  168. listener.id = commons.getUID();
  169. }
  170. selector = selector.replace(SCOPE_SELECTOR, `#${listener.id} > `);
  171. }
  172.  
  173. if(element.matches(selector)) {
  174. listener[handler].call(listener, new Event({
  175. target: element,
  176. detail: detail,
  177. type: message,
  178. currentTarget: listener
  179. }));
  180. }
  181. }
  182. });
  183. }
  184.  
  185. /**
  186. * post the provided message to all listener,
  187. * along with validating silencing and storing in queue
  188. * @param {String} message which should be posted
  189. * @param {Object} additional detail which needs to be posted.
  190. * @private
  191. */
  192. postMessage(message, detail) {
  193. if(this.isSilenced) {
  194. return;
  195. }
  196.  
  197. if(!this.isConnected) {
  198. this._addMessageToQueue(message, detail);
  199. return;
  200. }
  201.  
  202. // element got disconnect and messenger not notified.
  203. if(!this._element.isConnected) {
  204. // disconnect messenger and again post the same message,
  205. // message will get store in queue.
  206. this.disconnect();
  207. this.postMessage(message, detail);
  208. return;
  209. }
  210.  
  211. this._postMessage(message, detail);
  212. }
  213.  
  214. /**
  215. * element should call this method when they are disconnected from DOM.
  216. * Its the responsibility of the element to call this hook
  217. * @private
  218. */
  219. disconnect() {
  220. if(this.isConnected) {
  221. this._connected = false;
  222. this._clearListeners();
  223. this._clearQueue();
  224. }
  225. }
  226. }
  227.  
  228. /**
  229. * This Event class is just a bogus class, current message callback aspects
  230. * actual event as a parameter, since we are directly calling the method instead
  231. * of triggering event, will pass an instance of this disguised object,
  232. * to avoid breaks.
  233. * This just disguise the most used functionality of event object
  234. * @private
  235. */
  236. class Event {
  237. constructor(options) {
  238. this._detail = options.detail;
  239. this._target = options.target;
  240. this._type = options.type;
  241. this._currentTarget = options.currentTarget;
  242. this._defaultPrevented = false;
  243. this._propagationStopped = false;
  244. this._immediatePropagationStopped = false;
  245. }
  246.  
  247. get detail() {
  248. return this._detail;
  249. }
  250.  
  251. get type() {
  252. return this._type;
  253. }
  254.  
  255. get target() {
  256. return this._target;
  257. }
  258.  
  259. get currentTarget() {
  260. return this._currentTarget;
  261. }
  262.  
  263. preventDefault() {
  264. this._defaultPrevented = true;
  265. }
  266.  
  267. stopPropagation() {
  268. this._propagationStopped = true;
  269. }
  270.  
  271. stopImmediatePropagation() {
  272. this._immediatePropagationStopped = true;
  273. }
  274. }
  275.  
  276. Messenger.Event = Event;
  277.  
  278. export default Messenger;