UNPKG

8.24 kBJavaScriptView Raw
1/*
2Copyright 2020 Adobe. All rights reserved.
3This file is licensed to you under the Apache License, Version 2.0 (the "License");
4you may not use this file except in compliance with the License. You may obtain a copy
5of the License at http://www.apache.org/licenses/LICENSE-2.0
6
7Unless required by applicable law or agreed to in writing, software distributed under
8the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
9OF ANY KIND, either express or implied. See the License for the specific language
10governing permissions and limitations under the License.
11*/
12
13const _ = require('../custom-lodash');
14const cloneDeep = _.cloneDeep;
15const get = _.get;
16
17const Item = require('./item');
18const Listener = require('./listener');
19const ListenerManager = require('./listenerManager');
20const CONSTANTS = require('./constants');
21const customMerge = require('./utils/customMerge');
22
23/**
24 * Manager
25 *
26 * @class Manager
27 * @classdesc Data Layer manager that augments the passed data layer array and handles eventing.
28 * @param {Object} config The Data Layer manager configuration.
29 */
30module.exports = function(config) {
31 const _config = config;
32 let _dataLayer = [];
33 let _state = {};
34 let _previousStateCopy = {};
35 let _listenerManager;
36
37 const DataLayerManager = {
38 getState: function() {
39 return _state;
40 },
41 getDataLayer: function() {
42 return _dataLayer;
43 },
44 getPreviousState: function() {
45 return _previousStateCopy;
46 }
47 };
48
49 _initialize();
50 _augment();
51 _processItems();
52
53 /**
54 * Initializes the data layer.
55 *
56 * @private
57 */
58 function _initialize() {
59 if (!Array.isArray(_config.dataLayer)) {
60 _config.dataLayer = [];
61 }
62
63 _dataLayer = _config.dataLayer;
64 _state = {};
65 _previousStateCopy = {};
66 _listenerManager = ListenerManager(DataLayerManager);
67 };
68
69 /**
70 * Updates the state with the item.
71 *
72 * @param {Item} item The item.
73 * @private
74 */
75 function _updateState(item) {
76 _previousStateCopy = cloneDeep(_state);
77 customMerge(_state, item.data);
78 };
79
80 /**
81 * Augments the data layer Array Object, overriding: push() and adding getState(), addEventListener and removeEventListener.
82 *
83 * @private
84 */
85 function _augment() {
86 /**
87 * Pushes one or more items to the data layer.
88 *
89 * @param {...ItemConfig} var_args The items to add to the data layer.
90 * @returns {Number} The length of the data layer following push.
91 */
92 _dataLayer.push = function(var_args) { /* eslint-disable-line camelcase */
93 const pushArguments = arguments;
94 const filteredArguments = arguments;
95
96 Object.keys(pushArguments).forEach(function(key) {
97 const itemConfig = pushArguments[key];
98 const item = Item(itemConfig);
99
100 if (!item.valid) {
101 delete filteredArguments[key];
102 }
103 switch (item.type) {
104 case CONSTANTS.itemType.DATA:
105 case CONSTANTS.itemType.EVENT: {
106 _processItem(item);
107 break;
108 }
109 case CONSTANTS.itemType.FCTN: {
110 delete filteredArguments[key];
111 _processItem(item);
112 break;
113 }
114 case CONSTANTS.itemType.LISTENER_ON:
115 case CONSTANTS.itemType.LISTENER_OFF: {
116 delete filteredArguments[key];
117 }
118 }
119 });
120
121 if (filteredArguments[0]) {
122 return Array.prototype.push.apply(this, filteredArguments);
123 }
124 };
125
126 /**
127 * Returns a deep copy of the data layer state or of the object defined by the path.
128 *
129 * @param {Array|String} path The path of the property to get.
130 * @returns {*} Returns a deep copy of the resolved value if a path is passed, a deep copy of the data layer state otherwise.
131 */
132 _dataLayer.getState = function(path) {
133 if (path) {
134 return get(cloneDeep(_state), path);
135 }
136 return cloneDeep(_state);
137 };
138
139 /**
140 * Sets up a function that will be called whenever the specified event is triggered.
141 *
142 * @param {String} type A case-sensitive string representing the event type to listen for.
143 * @param {Function} listener A function that is called when the event of the specified type occurs.
144 * @param {Object} [options] Optional characteristics of the event listener. Available options:
145 * - {String} path The path of the object to listen to.
146 * - {String} scope The listener scope. Possible values:
147 * - {String} past The listener is triggered for past events.
148 * - {String} future The listener is triggered for future events.
149 * - {String} all The listener is triggered for past and future events (default value).
150 */
151 _dataLayer.addEventListener = function(type, listener, options) {
152 const eventListenerItem = Item({
153 on: type,
154 handler: listener,
155 scope: options && options.scope,
156 path: options && options.path
157 });
158
159 _processItem(eventListenerItem);
160 };
161
162 /**
163 * Removes an event listener previously registered with addEventListener().
164 *
165 * @param {String} type A case-sensitive string representing the event type to listen for.
166 * @param {Function} [listener] Optional function that is to be removed.
167 */
168 _dataLayer.removeEventListener = function(type, listener) {
169 const eventListenerItem = Item({
170 off: type,
171 handler: listener
172 });
173
174 _processItem(eventListenerItem);
175 };
176 };
177
178 /**
179 * Processes all items that already exist on the stack.
180 *
181 * @private
182 */
183 function _processItems() {
184 for (let i = 0; i < _dataLayer.length; i++) {
185 const item = Item(_dataLayer[i], i);
186
187 _processItem(item);
188
189 // remove event listener or invalid item from the data layer array
190 if (item.type === CONSTANTS.itemType.LISTENER_ON ||
191 item.type === CONSTANTS.itemType.LISTENER_OFF ||
192 item.type === CONSTANTS.itemType.FCTN ||
193 !item.valid) {
194 _dataLayer.splice(i, 1);
195 i--;
196 }
197 }
198 };
199
200 /**
201 * Processes an item pushed to the stack.
202 *
203 * @param {Item} item The item to process.
204 * @private
205 */
206 function _processItem(item) {
207 if (!item.valid) {
208 const message = 'The following item cannot be handled by the data layer ' +
209 'because it does not have a valid format: ' +
210 JSON.stringify(item.config);
211 console.error(message);
212 return;
213 }
214
215 /**
216 * Returns all items before the provided one.
217 *
218 * @param {Item} item The item.
219 * @returns {Array<Item>} The items before.
220 * @private
221 */
222 function _getBefore(item) {
223 if (!(_dataLayer.length === 0 || item.index > _dataLayer.length - 1)) {
224 return _dataLayer.slice(0, item.index).map(itemConfig => Item(itemConfig));
225 }
226 return [];
227 }
228
229 const typeProcessors = {
230 data: function(item) {
231 _updateState(item);
232 _listenerManager.triggerListeners(item);
233 },
234 fctn: function(item) {
235 item.config.call(_dataLayer, _dataLayer);
236 },
237 event: function(item) {
238 if (item.data) {
239 _updateState(item);
240 }
241 _listenerManager.triggerListeners(item);
242 },
243 listenerOn: function(item) {
244 const listener = Listener(item);
245 switch (listener.scope) {
246 case CONSTANTS.listenerScope.PAST: {
247 for (const registeredItem of _getBefore(item)) {
248 _listenerManager.triggerListener(listener, registeredItem);
249 }
250 break;
251 }
252 case CONSTANTS.listenerScope.FUTURE: {
253 _listenerManager.register(listener);
254 break;
255 }
256 case CONSTANTS.listenerScope.ALL: {
257 const registered = _listenerManager.register(listener);
258 if (registered) {
259 for (const registeredItem of _getBefore(item)) {
260 _listenerManager.triggerListener(listener, registeredItem);
261 }
262 }
263 }
264 }
265 },
266 listenerOff: function(item) {
267 _listenerManager.unregister(Listener(item));
268 }
269 };
270
271 typeProcessors[item.type](item);
272 };
273
274 return DataLayerManager;
275};