Patched to Drupal 8.4.8 level. See https://www.drupal.org/sa-core-2018-004 and patch...
[yaffs-website] / vendor / jcalderonzumba / gastonjs / src / Client / agent.js
1 var PoltergeistAgent;
2
3 PoltergeistAgent = (function () {
4   function PoltergeistAgent() {
5     this.elements = [];
6     this.nodes = {};
7   }
8
9   /**
10    * Executes an external call done from the web page class
11    * @param name
12    * @param args
13    * @return {*}
14    */
15   PoltergeistAgent.prototype.externalCall = function (name, args) {
16     var error;
17     try {
18       return {
19         value: this[name].apply(this, args)
20       };
21     } catch (_error) {
22       error = _error;
23       return {
24         error: {
25           message: error.toString(),
26           stack: error.stack
27         }
28       };
29     }
30   };
31
32   /**
33    * Object stringifycation
34    * @param object
35    * @return {*}
36    */
37   PoltergeistAgent.stringify = function (object) {
38     var error;
39     try {
40       return JSON.stringify(object, function (key, value) {
41         if (Array.isArray(this[key])) {
42           return this[key];
43         } else {
44           return value;
45         }
46       });
47     } catch (_error) {
48       error = _error;
49       if (error instanceof TypeError) {
50         return '"(cyclic structure)"';
51       } else {
52         throw error;
53       }
54     }
55   };
56
57   /**
58    * Name speaks for itself
59    * @return {string}
60    */
61   PoltergeistAgent.prototype.currentUrl = function () {
62     return encodeURI(decodeURI(window.location.href));
63   };
64
65   /**
66    *  Given a method of selection (xpath or css), a selector and a possible element to search
67    *  tries to find the elements that matches such selection
68    * @param method
69    * @param selector
70    * @param within
71    * @return {Array}
72    */
73   PoltergeistAgent.prototype.find = function (method, selector, within) {
74     var elementForXpath, error, i, results, xpath, _i, _len, _results;
75     if (within == null) {
76       within = document;
77     }
78     try {
79       if (method === "xpath") {
80         xpath = document.evaluate(selector, within, null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null);
81         results = (function () {
82           var _i, _ref, _results;
83           _results = [];
84           for (i = _i = 0, _ref = xpath.snapshotLength; 0 <= _ref ? _i < _ref : _i > _ref; i = 0 <= _ref ? ++_i : --_i) {
85             _results.push(xpath.snapshotItem(i));
86           }
87           return _results;
88         })();
89       } else {
90         results = within.querySelectorAll(selector);
91       }
92       _results = [];
93       for (_i = 0, _len = results.length; _i < _len; _i++) {
94         elementForXpath = results[_i];
95         _results.push(this.register(elementForXpath));
96       }
97       return _results;
98     } catch (_error) {
99       error = _error;
100       if (error.code === DOMException.SYNTAX_ERR || error.code === 51) {
101         throw new PoltergeistAgent.InvalidSelector;
102       } else {
103         throw error;
104       }
105     }
106   };
107
108   /**
109    *  Register the element in the agent
110    * @param element
111    * @return {number}
112    */
113   PoltergeistAgent.prototype.register = function (element) {
114     this.elements.push(element);
115     return this.elements.length - 1;
116   };
117
118   /**
119    *  Gets the size of the document
120    * @return {{height: number, width: number}}
121    */
122   PoltergeistAgent.prototype.documentSize = function () {
123     return {
124       height: document.documentElement.scrollHeight || document.documentElement.clientHeight,
125       width: document.documentElement.scrollWidth || document.documentElement.clientWidth
126     };
127   };
128
129   /**
130    * Gets a Node by a given id
131    * @param id
132    * @return {PoltergeistAgent.Node}
133    */
134   PoltergeistAgent.prototype.get = function (id) {
135     if (typeof this.nodes[id] == "undefined" || this.nodes[id] === null) {
136       //Let's try now the elements approach
137       if (typeof this.elements[id] == "undefined" || this.elements[id] === null) {
138         throw new PoltergeistAgent.ObsoleteNode;
139       }
140       return new PoltergeistAgent.Node(this, this.elements[id]);
141     }
142
143     return this.nodes[id];
144   };
145
146   /**
147    * Calls a Node agent function from the Node caller via delegates
148    * @param id
149    * @param name
150    * @param args
151    * @return {*}
152    */
153   PoltergeistAgent.prototype.nodeCall = function (id, name, args) {
154     var node;
155
156     node = this.get(id);
157     if (node.isObsolete()) {
158       throw new PoltergeistAgent.ObsoleteNode;
159     }
160     //TODO: add some error control here, we might not be able to call name function
161     return node[name].apply(node, args);
162   };
163
164   PoltergeistAgent.prototype.beforeUpload = function (id) {
165     return this.get(id).setAttribute('_poltergeist_selected', '');
166   };
167
168   PoltergeistAgent.prototype.afterUpload = function (id) {
169     return this.get(id).removeAttribute('_poltergeist_selected');
170   };
171
172   PoltergeistAgent.prototype.clearLocalStorage = function () {
173     //TODO: WTF where is variable...
174     return localStorage.clear();
175   };
176
177   return PoltergeistAgent;
178
179 })();
180
181 PoltergeistAgent.ObsoleteNode = (function () {
182   function ObsoleteNode() {
183   }
184
185   ObsoleteNode.prototype.toString = function () {
186     return "PoltergeistAgent.ObsoleteNode";
187   };
188
189   return ObsoleteNode;
190
191 })();
192
193 PoltergeistAgent.InvalidSelector = (function () {
194   function InvalidSelector() {
195   }
196
197   InvalidSelector.prototype.toString = function () {
198     return "PoltergeistAgent.InvalidSelector";
199   };
200
201   return InvalidSelector;
202
203 })();
204
205 PoltergeistAgent.Node = (function () {
206
207   Node.EVENTS = {
208     FOCUS: ['blur', 'focus', 'focusin', 'focusout'],
209     MOUSE: ['click', 'dblclick', 'mousedown', 'mouseenter', 'mouseleave', 'mousemove', 'mouseover', 'mouseout', 'mouseup', 'contextmenu'],
210     FORM: ['submit']
211   };
212
213   function Node(agent, element) {
214     this.agent = agent;
215     this.element = element;
216   }
217
218   /**
219    * Give me the node id of the parent of this node
220    * @return {number}
221    */
222   Node.prototype.parentId = function () {
223     return this.agent.register(this.element.parentNode);
224   };
225
226   /**
227    * Returns all the node parents ids up to first child of the dom
228    * @return {Array}
229    */
230   Node.prototype.parentIds = function () {
231     var ids, parent;
232     ids = [];
233     parent = this.element.parentNode;
234     while (parent !== document) {
235       ids.push(this.agent.register(parent));
236       parent = parent.parentNode;
237     }
238     return ids;
239   };
240
241   /**
242    * Finds and returns the node ids that matches the selector within this node
243    * @param method
244    * @param selector
245    * @return {Array}
246    */
247   Node.prototype.find = function (method, selector) {
248     return this.agent.find(method, selector, this.element);
249   };
250
251   /**
252    * Checks whether the node is obsolete or not
253    * @return boolean
254    */
255   Node.prototype.isObsolete = function () {
256     var obsolete;
257
258     obsolete = function (element) {
259       if (element.parentNode != null) {
260         if (element.parentNode === document) {
261           return false;
262         } else {
263           return obsolete(element.parentNode);
264         }
265       } else {
266         return true;
267       }
268     };
269
270     return obsolete(this.element);
271   };
272
273   Node.prototype.changed = function () {
274     var event;
275     event = document.createEvent('HTMLEvents');
276     event.initEvent('change', true, false);
277     return this.element.dispatchEvent(event);
278   };
279
280   Node.prototype.input = function () {
281     var event;
282     event = document.createEvent('HTMLEvents');
283     event.initEvent('input', true, false);
284     return this.element.dispatchEvent(event);
285   };
286
287   Node.prototype.keyupdowned = function (eventName, keyCode) {
288     var event;
289     event = document.createEvent('UIEvents');
290     event.initEvent(eventName, true, true);
291     event.keyCode = keyCode;
292     event.which = keyCode;
293     event.charCode = 0;
294     return this.element.dispatchEvent(event);
295   };
296
297   Node.prototype.keypressed = function (altKey, ctrlKey, shiftKey, metaKey, keyCode, charCode) {
298     var event;
299     event = document.createEvent('UIEvents');
300     event.initEvent('keypress', true, true);
301     event.window = this.agent.window;
302     event.altKey = altKey;
303     event.ctrlKey = ctrlKey;
304     event.shiftKey = shiftKey;
305     event.metaKey = metaKey;
306     event.keyCode = keyCode;
307     event.charCode = charCode;
308     event.which = keyCode;
309     return this.element.dispatchEvent(event);
310   };
311
312   /**
313    * Tells if the node is inside the body of the document and not somewhere else
314    * @return {boolean}
315    */
316   Node.prototype.insideBody = function () {
317     return this.element === document.body || document.evaluate('ancestor::body', this.element, null, XPathResult.BOOLEAN_TYPE, null).booleanValue;
318   };
319
320   /**
321    * Returns all text visible or not of the node
322    * @return {string}
323    */
324   Node.prototype.allText = function () {
325     return this.element.textContent;
326   };
327
328   /**
329    * Returns the inner html our outer
330    * @returns {string}
331    */
332   Node.prototype.allHTML = function (type) {
333     var returnType = type || 'inner';
334
335     if (returnType === "inner") {
336       return this.element.innerHTML;
337     }
338
339     if (returnType === "outer") {
340       if (this.element.outerHTML) {
341         return this.element.outerHTML;
342       }
343       // polyfill:
344       var wrapper = document.createElement('div');
345       wrapper.appendChild(this.element.cloneNode(true));
346       return wrapper.innerHTML;
347     }
348
349     return '';
350   };
351
352   /**
353    * If the element is visible then we return the text
354    * @return {string}
355    */
356   Node.prototype.visibleText = function () {
357     if (!this.isVisible(null)) {
358       return null;
359     }
360
361     if (this.element.nodeName === "TEXTAREA") {
362       return this.element.textContent;
363     }
364
365     return this.element.innerText;
366   };
367
368   /**
369    * Deletes the actual text being represented by a selection object from the node's element DOM.
370    * @return {*}
371    */
372   Node.prototype.deleteText = function () {
373     var range;
374     range = document.createRange();
375     range.selectNodeContents(this.element);
376     window.getSelection().removeAllRanges();
377     window.getSelection().addRange(range);
378     return window.getSelection().deleteFromDocument();
379   };
380
381   /**
382    * Returns all the attributes {name:value} in the element
383    * @return {{}}
384    */
385   Node.prototype.getAttributes = function () {
386     var attributes, i, elementAttributes;
387
388     elementAttributes = this.element.attributes;
389     attributes = {};
390     for (i = 0; i < elementAttributes.length; i++) {
391       attributes[elementAttributes[i].name] = elementAttributes[i].value.replace("\n", "\\n");
392     }
393
394     return attributes;
395   };
396
397   /**
398    * Name speaks for it self, returns the value of a given attribute by name
399    * @param name
400    * @return {string}
401    */
402   Node.prototype.getAttribute = function (name) {
403     if (name === 'checked' || name === 'selected' || name === 'multiple') {
404       return this.element[name];
405     }
406     return this.element.getAttribute(name);
407   };
408
409   /**
410    * Scrolls the current element into the visible area of the browser window
411    * @return {*}
412    */
413   Node.prototype.scrollIntoView = function () {
414     return this.element.scrollIntoViewIfNeeded();
415   };
416
417   /**
418    *  Returns the element.value property with special treatment if the element is a select
419    * @return {*}
420    */
421   Node.prototype.value = function () {
422     var options, i, values;
423
424     if (this.element.tagName.toLowerCase() === 'select' && this.element.multiple) {
425       values = [];
426       options = this.element.children;
427       for (i = 0; i < options.length; i++) {
428         if (options[i].selected) {
429           values.push(options[i].value);
430         }
431       }
432       return values;
433     }
434
435     return this.element.value;
436   };
437
438   /**
439    * Sets a given value in the element value property by simulation key interaction
440    * @param value
441    * @return {*}
442    */
443   Node.prototype.set = function (value) {
444     var char, keyCode, i, len;
445
446     if (this.element.readOnly) {
447       return null;
448     }
449
450     //respect the maxLength property if present
451     if (this.element.maxLength >= 0) {
452       value = value.substr(0, this.element.maxLength);
453     }
454
455     this.element.value = '';
456     this.trigger('focus');
457
458     if (this.element.type === 'number') {
459       this.element.value = value;
460     } else {
461       for (i = 0, len = value.length; i < len; i++) {
462         char = value[i];
463         keyCode = this.characterToKeyCode(char);
464         this.keyupdowned('keydown', keyCode);
465         this.element.value += char;
466         this.keypressed(false, false, false, false, char.charCodeAt(0), char.charCodeAt(0));
467         this.keyupdowned('keyup', keyCode);
468       }
469     }
470
471     this.changed();
472     this.input();
473
474     return this.trigger('blur');
475   };
476
477   /**
478    * Is the node multiple
479    * @return {boolean}
480    */
481   Node.prototype.isMultiple = function () {
482     return this.element.multiple;
483   };
484
485   /**
486    * Sets the value of an attribute given by name
487    * @param name
488    * @param value
489    * @return {boolean}
490    */
491   Node.prototype.setAttribute = function (name, value) {
492     if (value === null) {
493       return this.removeAttribute(name);
494     }
495
496     this.element.setAttribute(name, value);
497     return true;
498   };
499
500   /**
501    *  Removes and attribute by name
502    * @param name
503    * @return {boolean}
504    */
505   Node.prototype.removeAttribute = function (name) {
506     this.element.removeAttribute(name);
507     return true;
508   };
509
510   /**
511    *  Selects the current node
512    * @param value
513    * @return {boolean}
514    */
515   Node.prototype.select = function (value) {
516     if (value === false && !this.element.parentNode.multiple) {
517       return false;
518     }
519
520     this.element.selected = value;
521     this.changed();
522     return true;
523   };
524
525   /**
526    * Selects the radio button that has the defined value
527    * @param value
528    * @return {boolean}
529    */
530   Node.prototype.selectRadioValue = function (value) {
531     if (this.element.value == value) {
532       this.element.checked = true;
533       this.trigger('focus');
534       this.trigger('click');
535       this.changed();
536       return true;
537     }
538
539     var formElements = this.element.form.elements;
540     var name = this.element.getAttribute('name');
541     var element, i;
542
543     var deselectAllRadios = function (elements, radioName) {
544       var inputRadioElement;
545
546       for (i = 0; i < elements.length; i++) {
547         inputRadioElement = elements[i];
548         if (inputRadioElement.tagName.toLowerCase() == 'input' && inputRadioElement.type.toLowerCase() == 'radio' && inputRadioElement.name == radioName) {
549           inputRadioElement.checked = false;
550         }
551       }
552     };
553
554     var radioChange = function (radioElement) {
555       var radioEvent;
556       radioEvent = document.createEvent('HTMLEvents');
557       radioEvent.initEvent('change', true, false);
558       return radioElement.dispatchEvent(radioEvent);
559     };
560
561     var radioClickEvent = function (radioElement, name) {
562       var radioEvent;
563       radioEvent = document.createEvent('MouseEvent');
564       radioEvent.initMouseEvent(name, true, true, window, 0, 0, 0, 0, 0, false, false, false, false, 0, null);
565       return radioElement.dispatchEvent(radioEvent);
566     };
567
568     if (!name) {
569       throw new Poltergeist.BrowserError('The radio button does not have the value "' + value + '"');
570     }
571
572     for (i = 0; i < formElements.length; i++) {
573       element = formElements[i];
574       if (element.tagName.toLowerCase() == 'input' && element.type.toLowerCase() == 'radio' && element.name === name) {
575         if (value === element.value) {
576           deselectAllRadios(formElements, name);
577           element.checked = true;
578           radioClickEvent(element, 'click');
579           radioChange(element);
580           return true;
581         }
582       }
583     }
584
585     throw new Poltergeist.BrowserError('The radio group "' + name + '" does not have an option "' + value + '"');
586   };
587
588   /**
589    *  Checks or uncheck a radio option
590    * @param value
591    * @return {boolean}
592    */
593   Node.prototype.checked = function (value) {
594     //TODO: add error control for the checked stuff
595     this.element.checked = value;
596     return true;
597   };
598
599   /**
600    * Returns the element tag name as is, no transformations done
601    * @return {string}
602    */
603   Node.prototype.tagName = function () {
604     return this.element.tagName;
605   };
606
607   /**
608    * Checks if the element is visible either by itself of because the parents are visible
609    * @param element
610    * @return {boolean}
611    */
612   Node.prototype.isVisible = function (element) {
613     var nodeElement = element || this.element;
614
615     if (window.getComputedStyle(nodeElement).display === 'none') {
616       return false;
617     } else if (nodeElement.parentElement) {
618       return this.isVisible(nodeElement.parentElement);
619     } else {
620       return true;
621     }
622   };
623
624   /**
625    * Is the node disabled for operations with it?
626    * @return {boolean}
627    */
628   Node.prototype.isDisabled = function () {
629     return this.element.disabled || this.element.tagName === 'OPTION' && this.element.parentNode.disabled;
630   };
631
632   /**
633    * Does the node contains the selections
634    * @return {boolean}
635    */
636   Node.prototype.containsSelection = function () {
637     var selectedNode;
638
639     selectedNode = document.getSelection().focusNode;
640     if (!selectedNode) {
641       return false;
642     }
643     //this magic number is NODE.TEXT_NODE
644     if (selectedNode.nodeType === 3) {
645       selectedNode = selectedNode.parentNode;
646     }
647
648     return this.element.contains(selectedNode);
649   };
650
651   /**
652    * Returns the offset of the node in relation to the current frame
653    * @return {{top: number, left: number}}
654    */
655   Node.prototype.frameOffset = function () {
656     var offset, rect, style, win;
657     win = window;
658     offset = {
659       top: 0,
660       left: 0
661     };
662     while (win.frameElement) {
663       rect = win.frameElement.getClientRects()[0];
664       style = win.getComputedStyle(win.frameElement);
665       win = win.parent;
666       offset.top += rect.top + parseInt(style.getPropertyValue("padding-top"), 10);
667       offset.left += rect.left + parseInt(style.getPropertyValue("padding-left"), 10);
668     }
669     return offset;
670   };
671
672   /**
673    * Returns the object position in relation to the window
674    * @return {{top: *, right: *, left: *, bottom: *, width: *, height: *}}
675    */
676   Node.prototype.position = function () {
677     var frameOffset, pos, rect;
678
679     rect = this.element.getClientRects()[0];
680     if (!rect) {
681       throw new PoltergeistAgent.ObsoleteNode;
682     }
683
684     frameOffset = this.frameOffset();
685     pos = {
686       top: rect.top + frameOffset.top,
687       right: rect.right + frameOffset.left,
688       left: rect.left + frameOffset.left,
689       bottom: rect.bottom + frameOffset.top,
690       width: rect.width,
691       height: rect.height
692     };
693
694     return pos;
695   };
696
697   /**
698    * Triggers a DOM event related to the node element
699    * @param name
700    * @return {boolean}
701    */
702   Node.prototype.trigger = function (name) {
703     var event;
704     if (Node.EVENTS.MOUSE.indexOf(name) !== -1) {
705       event = document.createEvent('MouseEvent');
706       event.initMouseEvent(name, true, true, window, 0, 0, 0, 0, 0, false, false, false, false, 0, null);
707     } else if (Node.EVENTS.FOCUS.indexOf(name) !== -1) {
708       event = this.obtainEvent(name);
709     } else if (Node.EVENTS.FORM.indexOf(name) !== -1) {
710       event = this.obtainEvent(name);
711     } else {
712       throw "Unknown event";
713     }
714     return this.element.dispatchEvent(event);
715   };
716
717   /**
718    * Creates a generic HTMLEvent to be use in the node element
719    * @param name
720    * @return {Event}
721    */
722   Node.prototype.obtainEvent = function (name) {
723     var event;
724     event = document.createEvent('HTMLEvents');
725     event.initEvent(name, true, true);
726     return event;
727   };
728
729   /**
730    * Does a check to see if the coordinates given
731    * match the node element or some of the parents chain
732    * @param x
733    * @param y
734    * @return {*}
735    */
736   Node.prototype.mouseEventTest = function (x, y) {
737     var elementForXpath, frameOffset, origEl;
738
739     frameOffset = this.frameOffset();
740     x -= frameOffset.left;
741     y -= frameOffset.top;
742
743     elementForXpath = origEl = document.elementFromPoint(x, y);
744     while (elementForXpath) {
745       if (elementForXpath === this.element) {
746         return {
747           status: 'success'
748         };
749       } else {
750         elementForXpath = elementForXpath.parentNode;
751       }
752     }
753
754     return {
755       status: 'failure',
756       selector: origEl && this.getSelector(origEl)
757     };
758   };
759
760   /**
761    * Returns the node selector in CSS style (NO xpath)
762    * @param elementForXpath
763    * @return {string}
764    */
765   Node.prototype.getSelector = function (elementForXpath) {
766     var className, selector, i, len, classNames;
767
768     selector = elementForXpath.tagName !== 'HTML' ? this.getSelector(elementForXpath.parentNode) + ' ' : '';
769     selector += elementForXpath.tagName.toLowerCase();
770
771     if (elementForXpath.id) {
772       selector += "#" + elementForXpath.id;
773     }
774
775     classNames = elementForXpath.classList;
776     for (i = 0, len = classNames.length; i < len; i++) {
777       className = classNames[i];
778       selector += "." + className;
779     }
780
781     return selector;
782   };
783
784   /**
785    * Returns the key code that represents the character
786    * @param character
787    * @return {number}
788    */
789   Node.prototype.characterToKeyCode = function (character) {
790     var code, specialKeys;
791     code = character.toUpperCase().charCodeAt(0);
792     specialKeys = {
793       96: 192,
794       45: 189,
795       61: 187,
796       91: 219,
797       93: 221,
798       92: 220,
799       59: 186,
800       39: 222,
801       44: 188,
802       46: 190,
803       47: 191,
804       127: 46,
805       126: 192,
806       33: 49,
807       64: 50,
808       35: 51,
809       36: 52,
810       37: 53,
811       94: 54,
812       38: 55,
813       42: 56,
814       40: 57,
815       41: 48,
816       95: 189,
817       43: 187,
818       123: 219,
819       125: 221,
820       124: 220,
821       58: 186,
822       34: 222,
823       60: 188,
824       62: 190,
825       63: 191
826     };
827     return specialKeys[code] || code;
828   };
829
830   /**
831    * Checks if one element is equal to other given by its node id
832    * @param other_id
833    * @return {boolean}
834    */
835   Node.prototype.isDOMEqual = function (other_id) {
836     return this.element === this.agent.get(other_id).element;
837   };
838
839   /**
840    * The following function allows one to pass an element and an XML document to find a unique string XPath expression leading back to that element.
841    * @param element
842    * @return {string}
843    */
844   Node.prototype.getXPathForElement = function (element) {
845     var elementForXpath = element || this.element;
846     var xpath = '';
847     var pos, tempitem2;
848
849     while (elementForXpath !== document.documentElement) {
850       pos = 0;
851       tempitem2 = elementForXpath;
852       while (tempitem2) {
853         if (tempitem2.nodeType === 1 && tempitem2.nodeName === elementForXpath.nodeName) { // If it is ELEMENT_NODE of the same name
854           pos += 1;
855         }
856         tempitem2 = tempitem2.previousSibling;
857       }
858
859       xpath = "*[name()='" + elementForXpath.nodeName + "' and namespace-uri()='" + (elementForXpath.namespaceURI === null ? '' : elementForXpath.namespaceURI) + "'][" + pos + ']' + '/' + xpath;
860
861       elementForXpath = elementForXpath.parentNode;
862     }
863
864     xpath = '/*' + "[name()='" + document.documentElement.nodeName + "' and namespace-uri()='" + (elementForXpath.namespaceURI === null ? '' : elementForXpath.namespaceURI) + "']" + '/' + xpath;
865     xpath = xpath.replace(/\/$/, '');
866     return xpath;
867   };
868
869   /**
870    * Deselect all the options for this element
871    */
872   Node.prototype.deselectAllOptions = function () {
873     //TODO: error control when the node is not a select node
874     var i, l = this.element.options.length;
875     for (i = 0; i < l; i++) {
876       this.element.options[i].selected = false;
877     }
878   };
879
880   return Node;
881
882 })();
883
884 window.__poltergeist = new PoltergeistAgent;
885
886 document.addEventListener('DOMContentLoaded', function () {
887   return console.log('__DOMContentLoaded');
888 });
889
890 window.confirm = function (message) {
891   return true;
892 };
893
894 window.prompt = function (message, _default) {
895   return _default || null;
896 };