1 |
|
2 |
|
3 |
|
4 |
|
5 |
|
6 |
|
7 |
|
8 |
|
9 |
|
10 |
|
11 |
|
12 |
|
13 | const _ = require('../custom-lodash');
|
14 | const cloneDeep = _.cloneDeep;
|
15 | const get = _.get;
|
16 |
|
17 | const Item = require('./item');
|
18 | const Listener = require('./listener');
|
19 | const ListenerManager = require('./listenerManager');
|
20 | const CONSTANTS = require('./constants');
|
21 | const 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 | */
|
30 | module.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 |
|
55 |
|
56 |
|
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 |
|
71 |
|
72 |
|
73 |
|
74 |
|
75 | function _updateState(item) {
|
76 | _previousStateCopy = cloneDeep(_state);
|
77 | customMerge(_state, item.data);
|
78 | };
|
79 |
|
80 | |
81 |
|
82 |
|
83 |
|
84 |
|
85 | function _augment() {
|
86 | |
87 |
|
88 |
|
89 |
|
90 |
|
91 |
|
92 | _dataLayer.push = function(var_args) {
|
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 |
|
128 |
|
129 |
|
130 |
|
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 |
|
164 |
|
165 |
|
166 |
|
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 |
|
180 |
|
181 |
|
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 |
|
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 |
|
202 |
|
203 |
|
204 |
|
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 |
|
217 |
|
218 |
|
219 |
|
220 |
|
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 | };
|