1var Penpal = (function () { 2 'use strict' 3 4 var MessageType 5 ;(function (MessageType) { 6 MessageType['Call'] = 'call' 7 MessageType['Reply'] = 'reply' 8 MessageType['Syn'] = 'syn' 9 MessageType['SynAck'] = 'synAck' 10 MessageType['Ack'] = 'ack' 11 })(MessageType || (MessageType = {})) 12 13 var Resolution 14 ;(function (Resolution) { 15 Resolution['Fulfilled'] = 'fulfilled' 16 Resolution['Rejected'] = 'rejected' 17 })(Resolution || (Resolution = {})) 18 19 var ErrorCode 20 ;(function (ErrorCode) { 21 ErrorCode['ConnectionDestroyed'] = 'ConnectionDestroyed' 22 ErrorCode['ConnectionTimeout'] = 'ConnectionTimeout' 23 ErrorCode['NoIframeSrc'] = 'NoIframeSrc' 24 })(ErrorCode || (ErrorCode = {})) 25 26 var NativeErrorName 27 ;(function (NativeErrorName) { 28 NativeErrorName['DataCloneError'] = 'DataCloneError' 29 })(NativeErrorName || (NativeErrorName = {})) 30 31 var NativeEventType 32 ;(function (NativeEventType) { 33 NativeEventType['Message'] = 'message' 34 })(NativeEventType || (NativeEventType = {})) 35 36 var createDestructor = (localName, log) => { 37 const callbacks = [] 38 let destroyed = false 39 return { 40 destroy(error) { 41 if (!destroyed) { 42 destroyed = true 43 log(`${localName}: Destroying connection`) 44 callbacks.forEach((callback) => { 45 callback(error) 46 }) 47 } 48 }, 49 50 onDestroy(callback) { 51 destroyed ? callback() : callbacks.push(callback) 52 }, 53 } 54 } 55 56 var createLogger = (debug) => { 57 /** 58 * Logs a message if debug is enabled. 59 */ 60 return (...args) => { 61 if (debug) { 62 console.debug('[Penpal]', ...args) 63 } 64 } 65 } 66 67 const DEFAULT_PORT_BY_PROTOCOL = { 68 'http:': '80', 69 'https:': '443', 70 } 71 const URL_REGEX = /^(https?:)?\/\/([^/:]+)?(:(\d+))?/ 72 const opaqueOriginSchemes = ['file:', 'data:'] 73 /** 74 * Converts a src value into an origin. 75 */ 76 77 var getOriginFromSrc = (src) => { 78 if (src && opaqueOriginSchemes.find((scheme) => src.startsWith(scheme))) { 79 // The origin of the child document is an opaque origin and its 80 // serialization is "null" 81 // https://html.spec.whatwg.org/multipage/origin.html#origin 82 return 'null' 83 } // Note that if src is undefined, then srcdoc is being used instead of src 84 // and we can follow this same logic below to get the origin of the parent, 85 // which is the origin that we will need to use. 86 87 const location = document.location 88 const regexResult = URL_REGEX.exec(src) 89 let protocol 90 let hostname 91 let port 92 93 if (regexResult) { 94 // It's an absolute URL. Use the parsed info. 95 // regexResult[1] will be undefined if the URL starts with // 96 protocol = regexResult[1] ? regexResult[1] : location.protocol 97 hostname = regexResult[2] 98 port = regexResult[4] 99 } else { 100 // It's a relative path. Use the current location's info. 101 protocol = location.protocol 102 hostname = location.hostname 103 port = location.port 104 } // If the port is the default for the protocol, we don't want to add it to the origin string 105 // or it won't match the message's event.origin. 106 107 const portSuffix = port && port !== DEFAULT_PORT_BY_PROTOCOL[protocol] ? `:${port}` : '' 108 return `${protocol}//${hostname}${portSuffix}` 109 } 110 111 /** 112 * Converts an error object into a plain object. 113 */ 114 const serializeError = ({ name, message, stack }) => ({ 115 name, 116 message, 117 stack, 118 }) 119 /** 120 * Converts a plain object into an error object. 121 */ 122 123 const deserializeError = (obj) => { 124 const deserializedError = new Error() // @ts-ignore 125 126 Object.keys(obj).forEach((key) => (deserializedError[key] = obj[key])) 127 return deserializedError 128 } 129 130 /** 131 * Listens for "call" messages coming from the remote, executes the corresponding method, and 132 * responds with the return value. 133 */ 134 135 var connectCallReceiver = (info, serializedMethods, log) => { 136 const { localName, local, remote, originForSending, originForReceiving } = info 137 let destroyed = false 138 139 const handleMessageEvent = (event) => { 140 if (event.source !== remote || event.data.penpal !== MessageType.Call) { 141 return 142 } 143 144 if (originForReceiving !== '*' && event.origin !== originForReceiving) { 145 log( 146 `${localName} received message from origin ${event.origin} which did not match expected origin ${originForReceiving}` 147 ) 148 return 149 } 150 151 const callMessage = event.data 152 const { methodName, args, id } = callMessage 153 log(`${localName}: Received ${methodName}() call`) 154 155 const createPromiseHandler = (resolution) => { 156 return (returnValue) => { 157 log(`${localName}: Sending ${methodName}() reply`) 158 159 if (destroyed) { 160 // It's possible to throw an error here, but it would need to be thrown asynchronously 161 // and would only be catchable using window.onerror. This is because the consumer 162 // is merely returning a value from their method and not calling any function 163 // that they could wrap in a try-catch. Even if the consumer were to catch the error, 164 // the value of doing so is questionable. Instead, we'll just log a message. 165 log(`${localName}: Unable to send ${methodName}() reply due to destroyed connection`) 166 return 167 } 168 169 const message = { 170 penpal: MessageType.Reply, 171 id, 172 resolution, 173 returnValue, 174 } 175 176 if (resolution === Resolution.Rejected && returnValue instanceof Error) { 177 message.returnValue = serializeError(returnValue) 178 message.returnValueIsError = true 179 } 180 181 try { 182 remote.postMessage(message, originForSending) 183 } catch (err) { 184 // If a consumer attempts to send an object that's not cloneable (e.g., window), 185 // we want to ensure the receiver's promise gets rejected. 186 if (err.name === NativeErrorName.DataCloneError) { 187 const errorReplyMessage = { 188 penpal: MessageType.Reply, 189 id, 190 resolution: Resolution.Rejected, 191 returnValue: serializeError(err), 192 returnValueIsError: true, 193 } 194 remote.postMessage(errorReplyMessage, originForSending) 195 } 196 197 throw err 198 } 199 } 200 } 201 202 new Promise((resolve) => 203 resolve(serializedMethods[methodName].apply(serializedMethods, args)) 204 ).then(createPromiseHandler(Resolution.Fulfilled), createPromiseHandler(Resolution.Rejected)) 205 } 206 207 local.addEventListener(NativeEventType.Message, handleMessageEvent) 208 return () => { 209 destroyed = true 210 local.removeEventListener(NativeEventType.Message, handleMessageEvent) 211 } 212 } 213 214 let id = 0 215 /** 216 * @return {number} A unique ID (not universally unique) 217 */ 218 219 var generateId = () => ++id 220 221 const KEY_PATH_DELIMITER = '.' 222 223 const keyPathToSegments = (keyPath) => (keyPath ? keyPath.split(KEY_PATH_DELIMITER) : []) 224 225 const segmentsToKeyPath = (segments) => segments.join(KEY_PATH_DELIMITER) 226 227 const createKeyPath = (key, prefix) => { 228 const segments = keyPathToSegments(prefix || '') 229 segments.push(key) 230 return segmentsToKeyPath(segments) 231 } 232 /** 233 * Given a `keyPath`, set it to be `value` on `subject`, creating any intermediate 234 * objects along the way. 235 * 236 * @param {Object} subject The object on which to set value. 237 * @param {string} keyPath The key path at which to set value. 238 * @param {Object} value The value to store at the given key path. 239 * @returns {Object} Updated subject. 240 */ 241 242 const setAtKeyPath = (subject, keyPath, value) => { 243 const segments = keyPathToSegments(keyPath) 244 segments.reduce((prevSubject, key, idx) => {
245 if (typeof prevSubject[key] === 'undefined') { 246 prevSubject[key] = {} 247 } 248 249 if (idx === segments.length - 1) { 250 prevSubject[key] = value 251 } 252 253 return prevSubject[key] 254 }, subject) 255 return subject 256 } 257 /** 258 * Given a dictionary of (nested) keys to function, flatten them to a map 259 * from key path to function. 260 * 261 * @param {Object} methods The (potentially nested) object to serialize. 262 * @param {string} prefix A string with which to prefix entries. Typically not intended to be used by consumers. 263 * @returns {Object} An map from key path in `methods` to functions. 264 */ 265 266 const serializeMethods = (methods, prefix) => { 267 const flattenedMethods = {} 268 Object.keys(methods).forEach((key) => { 269 const value = methods[key] 270 const keyPath = createKeyPath(key, prefix) 271 272 if (typeof value === 'object') { 273 // Recurse into any nested children. 274 Object.assign(flattenedMethods, serializeMethods(value, keyPath)) 275 } 276 277 if (typeof value === 'function') { 278 // If we've found a method, expose it. 279 flattenedMethods[keyPath] = value 280 } 281 }) 282 return flattenedMethods 283 } 284 /** 285 * Given a map of key paths to functions, unpack the key paths to an object. 286 * 287 * @param {Object} flattenedMethods A map of key paths to functions to unpack. 288 * @returns {Object} A (potentially nested) map of functions. 289 */ 290 291 const deserializeMethods = (flattenedMethods) => { 292 const methods = {} 293 294 for (const keyPath in flattenedMethods) { 295 setAtKeyPath(methods, keyPath, flattenedMethods[keyPath]) 296 } 297 298 return methods 299 } 300 301 /** 302 * Augments an object with methods that match those defined by the remote. When these methods are 303 * called, a "call" message will be sent to the remote, the remote's corresponding method will be 304 * executed, and the method's return value will be returned via a message. 305 * @param {Object} callSender Sender object that should be augmented with methods. 306 * @param {Object} info Information about the local and remote windows. 307 * @param {Array} methodKeyPaths Key paths of methods available to be called on the remote. 308 * @param {Promise} destructionPromise A promise resolved when destroy() is called on the penpal 309 * connection. 310 * @returns {Object} The call sender object with methods that may be called. 311 */ 312 313 var connectCallSender = (callSender, info, methodKeyPaths, destroyConnection, log) => { 314 const { localName, local, remote, originForSending, originForReceiving } = info 315 let destroyed = false 316 log(`${localName}: Connecting call sender`) 317 318 const createMethodProxy = (methodName) => { 319 return (...args) => { 320 log(`${localName}: Sending ${methodName}() call`) // This handles the case where the iframe has been removed from the DOM 321 // (and therefore its window closed), the consumer has not yet 322 // called destroy(), and the user calls a method exposed by 323 // the remote. We detect the iframe has been removed and force 324 // a destroy() immediately so that the consumer sees the error saying 325 // the connection has been destroyed. We wrap this check in a try catch 326 // because Edge throws an "Object expected" error when accessing 327 // contentWindow.closed on a contentWindow from an iframe that's been 328 // removed from the DOM. 329 330 let iframeRemoved 331 332 try { 333 if (remote.closed) { 334 iframeRemoved = true 335 } 336 } catch { 337 iframeRemoved = true 338 } 339 340 if (iframeRemoved) { 341 destroyConnection() 342 } 343 344 if (destroyed) { 345 const error = new Error( 346 `Unable to send ${methodName}() call due ` + `to destroyed connection` 347 ) 348 error.code = ErrorCode.ConnectionDestroyed 349 throw error 350 } 351 352 return new Promise((resolve, reject) => { 353 const id = generateId() 354 355 const handleMessageEvent = (event) => { 356 if ( 357 event.source !== remote || 358 event.data.penpal !== MessageType.Reply || 359 event.data.id !== id 360 ) { 361 return 362 } 363 364 if (originForReceiving !== '*' && event.origin !== originForReceiving) { 365 log( 366 `${localName} received message from origin ${event.origin} which did not match expected origin ${originForReceiving}` 367 ) 368 return 369 } 370 371 const replyMessage = event.data 372 log(`${localName}: Received ${methodName}() reply`) 373 local.removeEventListener(NativeEventType.Message, handleMessageEvent) 374 let returnValue = replyMessage.returnValue 375 376 if (replyMessage.returnValueIsError) { 377 returnValue = deserializeError(returnValue) 378 } 379 380 ;(replyMessage.resolution === Resolution.Fulfilled ? resolve : reject)(returnValue) 381 } 382 383 local.addEventListener(NativeEventType.Message, handleMessageEvent) 384 const callMessage = { 385 penpal: MessageType.Call, 386 id, 387 methodName, 388 args, 389 } 390 remote.postMessage(callMessage, originForSending) 391 }) 392 } 393 } // Wrap each method in a proxy which sends it to the corresponding receiver. 394 395 const flattenedMethods = methodKeyPaths.reduce((api, name) => { 396 api[name] = createMethodProxy(name) 397 return api 398 }, {}) // Unpack the structure of the provided methods object onto the CallSender, exposing 399 // the methods in the same shape they were provided. 400 401 Object.assign(callSender, deserializeMethods(flattenedMethods)) 402 return () => { 403 destroyed = true 404 } 405 } 406 407 /** 408 * Handles an ACK handshake message. 409 */ 410 411 var handleAckMessageFactory = ( 412 serializedMethods, 413 childOrigin, 414 originForSending, 415 destructor, 416 log 417 ) => { 418 const { destroy, onDestroy } = destructor 419 let destroyCallReceiver 420 let receiverMethodNames // We resolve the promise with the call sender. If the child reconnects 421 // (for example, after refreshing or navigating to another page that 422 // uses Penpal, we'll update the call sender with methods that match the 423 // latest provided by the child. 424 425 const callSender = {} 426 return (event) => { 427 if (childOrigin !== '*' && event.origin !== childOrigin) { 428 log( 429 `Parent: Handshake - Received ACK message from origin ${event.origin} which did not match expected origin ${childOrigin}` 430 ) 431 return 432 } 433 434 log('Parent: Handshake - Received ACK') 435 const info = { 436 localName: 'Parent', 437 local: window, 438 remote: event.source, 439 originForSending: originForSending, 440 originForReceiving: childOrigin, 441 } // If the child reconnected, we need to destroy the prior call receiver 442 // before setting up a new one. 443 444 if (destroyCallReceiver) { 445 destroyCallReceiver() 446 } 447 448 destroyCallReceiver = connectCallReceiver(info, serializedMethods, log) 449 onDestroy(destroyCallReceiver) // If the child reconnected, we need to remove the methods from the 450 // previous call receiver off the sender. 451 452 if (receiverMethodNames) {
453 receiverMethodNames.forEach((receiverMethodName) => { 454 delete callSender[receiverMethodName] 455 }) 456 } 457 458 receiverMethodNames = event.data.methodNames 459 const destroyCallSender = connectCallSender( 460 callSender, 461 info, 462 receiverMethodNames, 463 destroy, 464 log 465 ) 466 onDestroy(destroyCallSender) 467 return callSender 468 } 469 } 470 471 /** 472 * Handles a SYN handshake message. 473 */ 474 475 var handleSynMessageFactory = (log, serializedMethods, childOrigin, originForSending) => { 476 return (event) => { 477 // Under specific timing circumstances, we can receive an event 478 // whose source is null. This seems to happen when the child iframe is 479 // removed from the DOM about the same time it has sent the SYN event. 480 // https://github.com/Aaronius/penpal/issues/85 481 if (!event.source) { 482 return 483 } 484 485 if (childOrigin !== '*' && event.origin !== childOrigin) { 486 log( 487 `Parent: Handshake - Received SYN message from origin ${event.origin} which did not match expected origin ${childOrigin}` 488 ) 489 return 490 } 491 492 log('Parent: Handshake - Received SYN, responding with SYN-ACK') 493 const synAckMessage = { 494 penpal: MessageType.SynAck, 495 methodNames: Object.keys(serializedMethods), 496 } 497 event.source.postMessage(synAckMessage, originForSending) 498 } 499 } 500 501 const CHECK_IFRAME_IN_DOC_INTERVAL = 60000 502 /** 503 * Monitors for iframe removal and destroys connection if iframe 504 * is found to have been removed from DOM. This is to prevent memory 505 * leaks when the iframe is removed from the document and the consumer 506 * hasn't called destroy(). Without this, event listeners attached to 507 * the window would stick around and since the event handlers have a 508 * reference to the iframe in their closures, the iframe would stick 509 * around too. 510 */ 511 512 var monitorIframeRemoval = (iframe, destructor) => { 513 const { destroy, onDestroy } = destructor 514 const checkIframeInDocIntervalId = setInterval(() => { 515 if (!iframe.isConnected) { 516 clearInterval(checkIframeInDocIntervalId) 517 destroy() 518 } 519 }, CHECK_IFRAME_IN_DOC_INTERVAL) 520 onDestroy(() => { 521 clearInterval(checkIframeInDocIntervalId) 522 }) 523 } 524 525 /** 526 * Starts a timeout and calls the callback with an error 527 * if the timeout completes before the stop function is called. 528 */ 529 530 var startConnectionTimeout = (timeout, callback) => { 531 let timeoutId 532 533 if (timeout !== undefined) { 534 timeoutId = window.setTimeout(() => { 535 const error = new Error(`Connection timed out after ${timeout}ms`) 536 error.code = ErrorCode.ConnectionTimeout 537 callback(error) 538 }, timeout) 539 } 540 541 return () => { 542 clearTimeout(timeoutId) 543 } 544 } 545 546 var validateIframeHasSrcOrSrcDoc = (iframe) => { 547 if (!iframe.src && !iframe.srcdoc) { 548 const error = new Error('Iframe must have src or srcdoc property defined.') 549 error.code = ErrorCode.NoIframeSrc 550 throw error 551 } 552 } 553 554 /** 555 * Attempts to establish communication with an iframe. 556 */ 557 558 var connectToChild = (options) => { 559 let { iframe, methods = {}, childOrigin, timeout, debug = false } = options 560 const log = createLogger(debug) 561 const destructor = createDestructor('Parent', log) 562 const { onDestroy, destroy } = destructor 563 564 if (!childOrigin) { 565 validateIframeHasSrcOrSrcDoc(iframe) 566 childOrigin = getOriginFromSrc(iframe.src) 567 } // If event.origin is "null", the remote protocol is file: or data: and we 568 // must post messages with "*" as targetOrigin when sending messages. 569 // https://developer.mozilla.org/en-US/docs/Web/API/Window/postMessage#Using_window.postMessage_in_extensions 570 571 const originForSending = childOrigin === 'null' ? '*' : childOrigin 572 const serializedMethods = serializeMethods(methods) 573 const handleSynMessage = handleSynMessageFactory( 574 log, 575 serializedMethods, 576 childOrigin, 577 originForSending 578 ) 579 const handleAckMessage = handleAckMessageFactory( 580 serializedMethods, 581 childOrigin, 582 originForSending, 583 destructor, 584 log 585 ) 586 const promise = new Promise((resolve, reject) => { 587 const stopConnectionTimeout = startConnectionTimeout(timeout, destroy) 588 589 const handleMessage = (event) => { 590 if (event.source !== iframe.contentWindow || !event.data) { 591 return 592 } 593 594 if (event.data.penpal === MessageType.Syn) { 595 handleSynMessage(event) 596 return 597 } 598 599 if (event.data.penpal === MessageType.Ack) { 600 const callSender = handleAckMessage(event) 601 602 if (callSender) { 603 stopConnectionTimeout() 604 resolve(callSender) 605 } 606 607 return 608 } 609 } 610 611 window.addEventListener(NativeEventType.Message, handleMessage) 612 log('Parent: Awaiting handshake') 613 monitorIframeRemoval(iframe, destructor) 614 onDestroy((error) => { 615 window.removeEventListener(NativeEventType.Message, handleMessage) 616 617 if (error) { 618 reject(error) 619 } 620 }) 621 }) 622 return { 623 promise, 624 625 destroy() { 626 // Don't allow consumer to pass an error into destroy. 627 destroy() 628 }, 629 } 630 } 631 632 /** 633 * Handles a SYN-ACK handshake message. 634 */ 635 636 var handleSynAckMessageFactory = (parentOrigin, serializedMethods, destructor, log) => { 637 const { destroy, onDestroy } = destructor 638 return (event) => { 639 let originQualifies = 640 parentOrigin instanceof RegExp 641 ? parentOrigin.test(event.origin) 642 : parentOrigin === '*' || parentOrigin === event.origin 643 644 if (!originQualifies) { 645 log( 646 `Child: Handshake - Received SYN-ACK from origin ${event.origin} which did not match expected origin ${parentOrigin}` 647 ) 648 return 649 } 650 651 log('Child: Handshake - Received SYN-ACK, responding with ACK') // If event.origin is "null", the remote protocol is file: or data: and we 652 // must post messages with "*" as targetOrigin when sending messages. 653 // https://developer.mozilla.org/en-US/docs/Web/API/Window/postMessage#Using_window.postMessage_in_extensions 654 655 const originForSending = event.origin === 'null' ? '*' : event.origin 656 const ackMessage = { 657 penpal: MessageType.Ack, 658 methodNames: Object.keys(serializedMethods), 659 }
660 window.parent.postMessage(ackMessage, originForSending) 661 const info = { 662 localName: 'Child', 663 local: window, 664 remote: window.parent, 665 originForSending, 666 originForReceiving: event.origin, 667 } 668 const destroyCallReceiver = connectCallReceiver(info, serializedMethods, log) 669 onDestroy(destroyCallReceiver) 670 const callSender = {} 671 const destroyCallSender = connectCallSender( 672 callSender, 673 info, 674 event.data.methodNames, 675 destroy, 676 log 677 ) 678 onDestroy(destroyCallSender) 679 return callSender 680 } 681 } 682 683 const areGlobalsAccessible = () => { 684 try { 685 clearTimeout() 686 } catch { 687 return false 688 } 689 690 return true 691 } 692 /** 693 * Attempts to establish communication with the parent window. 694 */ 695 696 var connectToParent = (options = {}) => { 697 const { parentOrigin = '*', methods = {}, timeout, debug = false } = options 698 const log = createLogger(debug) 699 const destructor = createDestructor('Child', log) 700 const { destroy, onDestroy } = destructor 701 const serializedMethods = serializeMethods(methods) 702 const handleSynAckMessage = handleSynAckMessageFactory( 703 parentOrigin, 704 serializedMethods, 705 destructor, 706 log 707 ) 708 709 const sendSynMessage = () => { 710 log('Child: Handshake - Sending SYN') 711 const synMessage = { 712 penpal: MessageType.Syn, 713 } 714 const parentOriginForSyn = parentOrigin instanceof RegExp ? '*' : parentOrigin 715 window.parent.postMessage(synMessage, parentOriginForSyn) 716 } 717 718 const promise = new Promise((resolve, reject) => { 719 const stopConnectionTimeout = startConnectionTimeout(timeout, destroy) 720 721 const handleMessage = (event) => { 722 // Under niche scenarios, we get into this function after 723 // the iframe has been removed from the DOM. In Edge, this 724 // results in "Object expected" errors being thrown when we 725 // try to access properties on window (global properties). 726 // For this reason, we try to access a global up front (clearTimeout) 727 // and if it fails we can assume the iframe has been removed 728 // and we ignore the message event. 729 if (!areGlobalsAccessible()) { 730 return 731 } 732 733 if (event.source !== parent || !event.data) { 734 return 735 } 736 737 if (event.data.penpal === MessageType.SynAck) { 738 const callSender = handleSynAckMessage(event) 739 740 if (callSender) { 741 window.removeEventListener(NativeEventType.Message, handleMessage) 742 stopConnectionTimeout() 743 resolve(callSender) 744 } 745 } 746 } 747 748 window.addEventListener(NativeEventType.Message, handleMessage) 749 sendSynMessage() 750 onDestroy((error) => { 751 window.removeEventListener(NativeEventType.Message, handleMessage) 752 753 if (error) { 754 reject(error) 755 } 756 }) 757 }) 758 return { 759 promise, 760 761 destroy() { 762 // Don't allow consumer to pass an error into destroy. 763 destroy() 764 }, 765 } 766 } 767 768 var indexForBundle = { 769 connectToChild, 770 connectToParent, 771 ErrorCode, 772 } 773 774 return indexForBundle 775})() 776 777let resizeObserver = null 778 779const PROPERTY_CONTAINER = 'aryeo_property_container' 780const PROPERTY_CONTAINER_IFRAME = 'aryeo_property_iframe' 781const PROPERTY_CONTAINER_BUTTON = 'aryeo_property_button' 782 783class Aryeo { 784 constructor(el) { 785 if (!el || el.dataset.aryeoInitialized) { 786 return 787 } 788 789 this.el = el 790 this.container = null 791 this.containerButton = null 792 this.propertyWebsiteIframe = null 793 794 this.IFRAME_URL = 'https://listing-embed.aryeo.com' 795 this.ACCESS_TOKEN = 'nEoDsFf0QR15Gu1FRfYzyAyJuslCHYfRqu6fFC2n' 796 this.HASH_ID = 'WLEJEV' 797 this.GROUP_ID = 'cf30ccea-588b-4b7d-8655-a2e5d251557a' 798 this.PARAMS = JSON.parse('{\"showBathFilter\":true,\"showBedFilter\":true,\"showPriceFilter\":true,\"showToolbar\":true,\"showStatusFilter\":false,\"statusFilterOption\":null}') 799 800 this.BANNER_HTML = '\"<div class=\\\"aryeo-listing-embed\\\">\\n <div class=\\\"ar-relative ar-bg-black ar-flex ar-items-center ar-justify-between\\\">\\n <button class=\\\"aryeo_property_button ar-flex ar-items-center ar-justify-center ar-p-5 ar-rounded-md ar-p-3 ar-hover:bg-primary-light ar-focus:outline-none ar-focus:ring-white ar-border-0 ar-text-white\\\">\\n <svg class=\\\"ar-w-6 ar-h-6 ar-mr-3\\\" fill=\\\"none\\\" stroke=\\\"currentColor\\\" viewBox=\\\"0 0 24 24\\\" xmlns=\\\"http:\\/\\/www.w3.org\\/2000\\/svg\\\">\\n <path stroke-linecap=\\\"round\\\" stroke-linejoin=\\\"round\\\" stroke-width=\\\"2\\\" d=\\\"M10 19l-7-7m0 0l7-7m-7 7h18\\\"><\\/path>\\n <\\/svg>\\n Back\\n <\\/button>\\n <button class=\\\"aryeo_property_button ar-flex ar-items-center ar-justify-center ar-p-5 ar-rounded-md ar-p-3 ar-hover:bg-primary-light ar-focus:outline-none ar-focus:ring-white ar-border-0 ar-text-white\\\">
800\\n <span class=\\\"ar-sr-only\\\">Dismiss<\\/span>\\n <svg class=\\\"ar-h-6 ar-w-6\\\" xmlns=\\\"http:\\/\\/www.w3.org\\/2000\\/svg\\\" fill=\\\"currentColor\\\" viewBox=\\\"0 0 24 24\\\" stroke=\\\"currentColor\\\" aria-hidden=\\\"true\\\">\\n <path stroke-linecap=\\\"round\\\" stroke-linejoin=\\\"round\\\" stroke-width=\\\"2\\\" d=\\\"M6 18L18 6M6 6l12 12\\\" \\/>\\n <\\/svg>\\n <\\/button>\\n <\\/div>\\n<\\/div>\"' 801 this.LOADER_HTML = '\"<div class=\\\"aryeo-listing-embed\\\">\\n <div class=\\\"ar-w-full ar-h-screen ar-flex ar-items-center ar-justify-center\\\">\\n <svg viewBox=\\\"0 0 38 38\\\" xmlns=\\\"http:\\/\\/www.w3.org\\/2000\\/svg\\\" stroke=\\\"currentColor\\\" class=\\\"ar-text-white ar-w-10 ar-h-10 ar-transition-all ar-ease-in-out ar-duration-500\\\">\\n <g fill=\\\"none\\\" fill-rule=\\\"evenodd\\\">\\n <g transform=\\\"translate(1 1)\\\" stroke-width=\\\"2\\\">\\n <circle stroke-opacity=\\\".5\\\" cx=\\\"18\\\" cy=\\\"18\\\" r=\\\"18\\\" \\/>\\n <path d=\\\"M36 18c0-9.94-8.06-18-18-18\\\">\\n <animateTransform attributeName=\\\"transform\\\" type=\\\"rotate\\\" from=\\\"0 18 18\\\" to=\\\"360 18 18\\\" dur=\\\"1s\\\" repeatCount=\\\"indefinite\\\" \\/>\\n <\\/path>\\n <\\/g>\\n <\\/g>\\n <\\/svg>\\n <\\/div>\\n<\\/div>\"' 802 803 const style = document.createElement('link') 804 style.type = 'text/css' 805 style.rel = 'stylesheet' 806 style.href = 'https://cdn.aryeo.com/build/assets/embed-nzvZPOh6.css' 807 style.setAttribute('id', 'aryeo_css') 808 809 document.head.appendChild(style) 810 811 this.setIframe() 812 el.dataset.aryeoInitialized = true 813 } 814 815 static create(el) { 816 return new Aryeo(el) 817 } 818 819 async setIframe() { 820 const params = this.buildParams() 821 822 const iframe = document.createElement('iframe') 823 824 iframe.src = `${this.IFRAME_URL}?${params}` 825 826 iframe.style = `border: none; width: 100%; height: 100%; overflow-y:hidden;` 827 828 iframe.setAttribute('id', this.HASH_ID) 829 iframe.title = 'Aryeo' 830 iframe.name = `aryeo_listing_iframe ${this.HASH_ID}` 831 iframe.setAttribute('scrolling', 'no') 832 833 const container = document.createElement('div') 834 835 container.appendChild(iframe) 836 837 this.el.parentNode.replaceChild(container, this.el) 838 this.setConnection(iframe) 839 } 840 841 displayPropertyIframe({ url, id }) { 842 if (document.getElementById(PROPERTY_CONTAINER)) return 843 if (!this.container) { 844 this.container = document.createElement('div') 845 this.container.setAttribute('id', PROPERTY_CONTAINER) 846 this.container.style.position = 'fixed' 847 this.container.style.top = '0' 848 this.container.style.left = '0' 849 this.container.style.width = '100%' 850 this.container.style.height = '100vh' 851 this.container.style.background = 'rgba(0,0,0, 0.75)' 852 this.container.style.zIndex = '99998' 853 this.container.style.overflowY = 'scroll' 854 this.container.style.display = 'flex' 855 this.container.style.flexDirection = 'column' 856 this.container.onclick = this.removeDisplayPropertyIframe 857 858 this.banner = document.createElement('div') 859 this.banner.innerHTML = JSON.parse(this.BANNER_HTML) 860 861 this.loader = document.createElement('div') 862 this.loader.innerHTML = JSON.parse(this.LOADER_HTML) 863 864 this.propertyWebsiteIframe = document.createElement('iframe') 865 this.propertyWebsiteIframe.setAttribute('id', PROPERTY_CONTAINER_IFRAME) 866 this.propertyWebsiteIframe.setAttribute('loading', 'lazy') 867 this.propertyWebsiteIframe.style.margin = '0 auto' 868 this.propertyWebsiteIframe.style.height = '100%' 869 this.propertyWebsiteIframe.style.border = 'none' 870 this.propertyWebsiteIframe.allowFullscreen = true 871 872 this.container.appendChild(this.banner) 873 this.container.appendChild(this.propertyWebsiteIframe) 874 this.container.appendChild(this.loader) 875 } 876 877 this.loader.style.display = 'block' 878 this.propertyWebsiteIframe.style.display = 'none' 879 880 document.body.style.overflowY = 'hidden' 881 882 this.propertyWebsiteIframe.style.transition = 'all 1s ease-out' 883 884 this.propertyWebsiteIframe.src = url 885 this.setPropertyConnection(this.propertyWebsiteIframe) 886 887 document.body.appendChild(this.container) 888 889 document.querySelectorAll(`.${PROPERTY_CONTAINER_BUTTON}`).forEach((el) => { 890 el.onclick = this.removeDisplayPropertyIframe 891 }) 892 893 setTimeout(() => { 894 this.loader.style.display = 'none' 895 this.propertyWebsiteIframe.style.display = 'block' 896 897 resizeObserver = new ResizeObserver(this.resizeContainer) 898 resizeObserver.observe(this.container) 899 }, 1500) 900 901 history.pushState({ listing: 1 }, 'Aryeo Property website', `?listing=${id}`) 902 } 903 904 resizeContainer() { 905 document.getElementById(PROPERTY_CONTAINER_IFRAME).style.width = 906 window.innerWidth > 960 ? '80%' : '100%' 907 } 908 909 removeDisplayPropertyIframe() { 910 history.pushState({}, document.title, location.pathname) 911 resizeObserver.disconnect() 912 document.body.removeChild(document.getElementById(PROPERTY_CONTAINER)) 913 document.body.style.overflowY = 'auto' 914 } 915 916 buildParams() { 917 const params = new URLSearchParams({ 918 ...this.PARAMS, 919 agentId: '', 920 listing: new URL(document.location).searchParams.get('listing'), 921 groupId: 'cf30ccea-588b-4b7d-8655-a2e5d251557a', 922 accessToken: 'nEoDsFf0QR15Gu1FRfYzyAyJuslCHYfRqu6fFC2n', 923 }) 924 925 return params.toString() 926 } 927 928 setPropertyConnection(iframe) { 929 Penpal.connectToChild({ 930 // The iframe to which a connection should be made 931 iframe, 932 // Methods the parent is exposing to the child 933 methods: { 934 resize(height) { 935 iframe.style.height = `${height}px` 936 }, 937 }, 938 }) 939 } 940 941 setConnection(element) { 942 const _self = this 943 const iframe = element 944 Penpal.connectToChild({ 945 // The iframe to which a connection should be made 946 iframe, 947 // Methods the parent is exposing to the child 948 methods: { 949 resize(height) { 950 iframe.style.height = `${height + 30}px` 951 }, 952 showProperty(url) {
953 _self.displayPropertyIframe(url) 954 }, 955 }, 956 }) 957 } 958} 959 960window.addEventListener('load', function () { 961 Aryeo.create(document.getElementById('WLEJEV')) 962})
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.