UNPKG

@stratusjs/idx

Version:

AngularJS idx/property Service and Components bundle to be used as an add on to StratusJS

1,452 lines (1,317 loc) 138 kB
/** * @file Idx Service @stratusjs/idx/idx * @example import '@stratusjs/idx/idx' */ const apiVersion = '3.13.0' // IDX Server Api version expected // Runtime import {clone, extend, get, isArray, isDate, isEmpty, isEqual, isNumber, isObject, isPlainObject, isString, uniqueId} from 'lodash' import {Stratus} from '@stratusjs/runtime/stratus' import { auto, element, material, IFilterOrderBy, IHttpService, IPromise, IRootScopeService, IScope, IQService, IWindowService } from 'angular' import {Model, ModelOptions, ModelSyncOptions} from '@stratusjs/angularjs/services/model' import {Collection, CollectionSyncOptions} from '@stratusjs/angularjs/services/collection' import { getHashBangParam, isJSON, LooseFunction, LooseObject, setHashBangParam } from '@stratusjs/core/misc' import {cookie} from '@stratusjs/core/environment' import {IdxDisclaimerScope} from '@stratusjs/idx/disclaimer/disclaimer.component' import {IdxMapScope} from '@stratusjs/idx/map/map.component' import {IdxPropertyListScope} from '@stratusjs/idx/property/list.component' import {IdxPropertySearchScope} from '@stratusjs/idx/property/search.component' import {IdxMemberListScope} from '@stratusjs/idx/member/list.component' // Stratus Preload import '@stratusjs/idx/listTrac' export interface IdxService { // Variables apiVersion: string // IDX Server version requested/designed for sharedValues: IdxSharedValue // Fetch Methods fetchMembers( uid: string, collectionVarName: string, options?: Pick<CompileFilterOptions, 'service' | 'where' | 'order' | 'page' | 'perPage' | 'fields' | 'images' | 'office'>, refresh?: boolean, listName?: string ): Promise<Collection> fetchOffices( uid: string, collectionVarName: string, options?: Pick<CompileFilterOptions, 'service' | 'where' | 'order' | 'page' | 'perPage' | 'fields' | 'images' | 'office' | 'managingBroker' | 'members'>, refresh?: boolean, listName?: string ): Promise<Collection> fetchProperties( uid: string, collectionVarName: string, options?: Pick<CompileFilterOptions, 'service' | 'where' | 'page' | 'perPage' | 'order' | 'fields' | 'images' | 'openhouses'>, refresh?: boolean, listName?: string ): Promise<Collection<Property>> fetchProperty( $scope: any, modelVarName: string, options?: Pick<CompileFilterOptions, 'service' | 'where' | 'fields' | 'images' | 'openhouses'> ): Promise<Model<Property>> // Instance Methods getDisclaimerInstance(disclaimerUid?: string): {[uid: string]: IdxDisclaimerScope} | null getListInstance(listUid: string, listType?: string): IdxPropertyListScope | IdxMemberListScope | IdxComponentScope | null getListInstanceLinks(listUid: string, listType?: string): (IdxPropertySearchScope | IdxMapScope | IdxComponentScope)[] getSearchInstanceLinks(searchUid: string, listType?: string): (IdxPropertyListScope | IdxComponentScope)[] ensureItemsAreArrays(itemParent: LooseObject, itemNames: string[]): void registerDetailsInstance( uid: string, moduleName: 'member' | 'office' | 'property', $scope: IdxDetailsScope ): void registerDisclaimerInstance(uid: string, $scope: IdxDisclaimerScope): void registerListInstance( uid: string, moduleName: 'member' | 'office' | 'property', $scope: IdxListScope ): void registerMapInstance(uid: string, $scope: IdxMapScope): void registerSearchInstance( uid: string, moduleName: 'member' | 'office' | 'property', $scope: IdxSearchScope, listUid?: string ): void unregisterDetailsInstance(uid: string, moduleName: 'member' | 'office' | 'property'): void // Url Option Methods getOptionsFromUrl(): UrlsOptionsObject getUrlOptions(listingOrSearch: 'Search' | 'Listing'): UrlWhereOptions getUrlOptionsPath(defaultOptions?: any): string refreshUrlOptions(defaultOptions: any): void setUrlOptions(listingOrSearch: 'Search' | 'Listing', options: any): void // Reusable Methods clearFieldInput(env: any): void devLog(...items: any): void emit( emitterName: string, $scope: IdxComponentScope, var1?: any, var2?: any, var3?: any ): void emitManual( emitterName: string, uid: string, $scope: IdxComponentScope, var1?: any, var2?: any, var3?: any ): void on( uid: string, emitterName: string, callback: IdxEmitter ): () => void getFriendlyStatus(property: Property, preferredStatus?: 'Closed' | 'Leased' | 'Rented'): string getFriendlyPriceLabel(property: Property): 'List Price' | 'Sale Price' | 'Lease Price' | 'Rent Price' | string getFullStatus(property: Property, preferredStatus: 'Closed' | 'Leased' | 'Rented'): 'Active' | 'Contingent' | 'Closed' | 'Leased' | 'Rented' | string getFullAddress(property: Property, encode?: boolean): string getGoogleMapsKey(): string | null getWebsiteMainContact(): { name: string email: string | null phone: string | null } | null getIdxServices(): number[] getMLSVariables(serviceIds?: number[]): MLSService[] getStreetAddress(property: Property): string // Other Unsorted Methods getContactVariables(): WidgetContact[] getDefaultWhereOptions(): WhereOptions setIdxServices(property: number[]): void setPageTitle(title?: string): void // Auth Methods setTokenURL(url: string): void tokenKeepAuth(keepAlive?: boolean): IPromise<void> getLastQueryTime(): Date|null getLastSessionTime(): Date|null // Scope helpers countArraysNotEmpty(arrayList: any[][]): number getInput(elementId: string): JQLite getNestedPathValue(currentNest: object | any, pathPieces: string[]): any getScopeValuePath(scope: IScope, scopeVarPath: string): any updateNestedPathValue(currentNest: object | any, pathPieces: object | any, value: any): Promise<string | any> updateScopeValuePath(scope: IScope, scopeVarPath: string, value: any): Promise<string | any> } export type IdxComponentScope = IScope & LooseObject<LooseFunction> & { uid: string elementId: string localDir: string Idx: IdxService on(emitterName: string, callback: IdxEmitter): () => void remove(): void } export type IdxDetailsScope<T = LooseObject> = IdxComponentScope & { model: Model<T> } export type IdxListScope<T = LooseObject> = IdxComponentScope & { collection: Collection<T> search(query?: CompileFilterOptions, refresh?: boolean, updateUrl?: boolean): Promise<Collection<T>> orderChange(order: string | string[], ev?: any): Promise<void> pageChange(pageNumber: number, ev?: any): Promise<void> pageNext(ev?: any): Promise<void> pagePrevious(ev?: any): Promise<void> displayModelDetails(model: T, ev?: any): void getPageModels(): T[] scrollToModel(model: T): void highlightModel(model: T, timeout?: number): void unhighlightModel(model: T): void } export type IdxSearchScope = IdxComponentScope & { listId: string listInitialized: boolean refreshSearchWidgetOptions(listScope?: IdxListScope): void search(force?: boolean): void } export type SelectionGroup = { name: string group: string[] } export interface UrlsOptionsObject { Listing: UrlWhereOptions // TODO convert to UrlWhereOptions Search: UrlWhereOptions } // This is what can be passed to the URL and useable options export interface UrlWhereOptions extends Omit<WhereOptions, 'Page' | 'Order'> { Page?: number Order?: any // TODO specify order } // Reusable Objects. Keys listed are not required, but help programmers define what exists/is possible export interface WhereOptions extends LooseObject { page?: undefined // Key being added to wrong type // Page?: undefined // Key being added to wrong type order?: undefined // Key being added to wrong type // Order?: undefined // Key being added to wrong type where?: undefined // Key being added to wrong type // Property ListingKey?: string, ListingId?: string[] | string, ListingType?: string[] | string, Status?: string[] | string, UnparsedAddress?: string[] | string, StreetAddress?: string[] | string, eStreetAddress?: string[] | string, City?: string[] | string, eCity?: string[] | string, PostalCode?: string[] | string, CityRegion?: string[] | string, eCityRegion?: string[] | string, CountyOrParish?: string[] | string, eCountyOrParish?: string[] | string, MLSAreaMajor?: string[] | string, eMLSAreaMajor?: string[] | string, MLSAreaMinor?: string[] | string, eMLSAreaMinor?: string[] | string, SubdivisionName?: string[] | string, eSubdivisionName?: string[] | string, ListPriceMin?: number | any, ListPriceMax?: number | any, Bathrooms?: number | any, // Previously BathroomsFullMin Bedrooms?: number | any, // Previously BedroomsTotalMin Location?: string, eLocation?: string, Neighborhood?: string[] | string, eNeighborhood?: string[] | string, AgentLicense?: string[] | string, // Converts on server to multiple checks OfficeNumber?: string[] | string, // Converts on server to multiple checks OfficeName?: string[] | string, // Converts on server to multiple checks OpenHouseOnly?: boolean, // Filters by only those with OpenHouses attached // Member MemberKey?: string, MemberStateLicense?: string, MemberNationalAssociationId?: string, MemberStatus?: string, MemberFullName?: string, // Office OfficeKey?: string, OfficeNationalAssociationId?: string, OfficeStatus?: string, } export interface CompileFilterOptions extends LooseObject { where?: WhereOptions, service?: number | number[], page?: number, perPage?: number | 20, order?: string | string[], fields?: string[], images?: boolean | { limit?: number, fields?: string[] | string, }, openhouses?: boolean | { limit?: number, fields?: string[] | string, }, office?: boolean | { limit?: number, fields?: string[] | string, }, managingBroker?: boolean | { limit?: number, fields?: string[] | string, }, members?: boolean | { limit?: number, fields?: string[] | string, } } export interface MLSService { id: number name: string disclaimer: string token?: string created?: string ttl?: number host?: string fetchTime: { Property: Date | null Media: Date | null Member: Date | null Office: Date | null OpenHouse: Date | null } analyticsEnabled: string[] logo: { default?: string tiny?: string // height 15px small?: string medium?: string large?: string } mandatoryLogo?: string[] } /** Sitetheory contact information */ export interface WidgetContact { name: string, emails: { Main?: string }, locations: { Main?: string }, phones: { Main?: string }, socialUrls: { Main?: string }, urls: { Main?: string } } /** Sitetheory provided integrations */ export interface WidgetIntegrations { analytics?: { googleAnalytics?: { accountId: string }, listTrac?: { accountId: string } }, maps?: { googleMaps?: { publicKey: string } } } /** Sitetheory provided preferences */ interface IdxSharedValue { contactUrl: string | null, contactCommentVariable: string | null, contact: WidgetContact | null, integrations: WidgetIntegrations } // Internal interface Session { services: {[serviceId: number]: MLSService}, lastCreated: Date, lastTtl: number expires?: Date contacts: WidgetContact[] } /** Sitetheory authentication token format */ interface TokenResponse { data: { contact?: { emails?: { Main?: string }, locations?: { Main?: string }, phones?: { Main?: string }, socialUrls?: {}, urls?: {}, }, contactName?: string, contactUrl?: string, contactCommentVariable?: string, errors?: any[], integrations?: WidgetIntegrations, lastCreated: Date, lastTtl: number, services?: MLSService[], site?: string, } } interface MongoWhereQuery { [key: string]: string | string[] | number | MongoWhereQuery[] | { inq?: string[] | number[], in?: string[] | number[], // Only for nested queries (alternative to inq) between?: number[], gte?: number, lte?: number, like?: string, options?: string, and?: MongoWhereQuery[], or?: MongoWhereQuery[] } } // Used locally to store a number of index prepared queries interface IncludeOptions { [key: string]: MongoIncludeQuery } // type MongoOrderQuery = string[] interface MongoOrderQuery extends Array<string> { } interface MongoIncludeQuery { relation: string, scope: { order?: string, fields?: '*' | string[], limit?: number, } } interface MongoFilterQuery { where: MongoWhereQuery, limit: number, skip: number, fields?: string[], order?: MongoOrderQuery, include?: MongoIncludeQuery[] | MongoIncludeQuery, count?: boolean, } export interface Office extends LooseObject { id: string OfficeKey: string OfficeMlsId?: string _OfficeNumber?: string _unmapped?: { [key: string]: unknown _highlight?: boolean } } export interface Member extends LooseObject { id: string MemberKey: string MemberFullName?: string MemberFirstName?: string MemberLastName?: string _MemberNumber?: string OfficeKey: string OfficeMlsId?: string _unmapped?: { [key: string]: unknown _highlight?: boolean } } export interface Media extends LooseObject { MediaKey: string MediaURL?: string } export interface OpenHouse extends LooseObject { OpenHouseKey: string OpenHouseStartTime: Date OpenHouseEndTime?: Date OpenHouseStatus?: string ShowingAgentFirstName?: string ShowingAgentLastName?: string } export interface Property extends LooseObject { id: string ListingKey: string ListingId: string MlsStatus: string StandardStatus: string PropertyType: string PropertySubType: string ListPrice: number ClosePrice: number // Time ModificationTimestamp: Date // TODO need to add basic types // Location UnparsedAddress?: string StreetNumberNumeric?: number StreetNumber?: string StreetDirPrefix?: string StreetName?: string StreetSuffix?: string StreetSuffixModifier?: string StreetDirSuffix?: string UnitNumber?: string City?: string StateOrProvince?: string PostalCode?: string Country?: string Latitude?: number Longitude?: number MapCoordinateSource?: string // Details BathroomsFull?: number BathroomsHalf?: number BathroomsOneQuarter?: number BathroomsPartial?: number BathroomsTotalInteger?: number BedroomsTotal?: number LivingArea?: number LivingAreaUnits?: string LotSizeArea?: number LotSizeUnits?: number LotSizeAcres?: number LotSizeSquareFeet?: number LeasableArea?: number LeasableAreaUnits?: string BuildingAreaTotal?: number BuildingAreaUnits?: string Stories?: number Levels?: string[] PublicRemarks?: string YearBuilt?: number PoolPrivateYN?: boolean InternetAddressDisplayYN?: boolean // Agent ListAgentFullName?: string ListAgentFirstName?: string ListAgentLastName?: string ListAgentDirectPhone?: string ListAgentOfficePhone?: string CoListAgentFullName?: string CoListAgentFirstName?: string CoListAgentLastName?: string BuyerAgentFullName?: string BuyerAgentFirstName?: string BuyerAgentLastName?: string CoBuyerAgentFullName?: string CoBuyerAgentFirstName?: string CoBuyerAgentLastName?: string [key: string]: unknown // Custom Images?: Media[] OpenHouses?: OpenHouse[] _ServiceId: number _Class: string _IsRental?: boolean _unmapped?: { [key: string]: unknown CoordinateModificationTimestamp?: Date _highlight?: boolean } } interface SearchObject { type: 'valueEquals' | // Input is a string, needs to equal another string or number field 'stringLike' | // Input is a string, needs to be similar to another string field 'stringLikeArray' | // Input is a string or array, one of which needs to be found similar to db string field 'stringIncludesArray' | // Input is a string or array, one of which needs to be found equal to db string field 'stringIncludesArrayAlternative' | // Input is a string or array, one of which needs to be found equal to db string field 'numberEqualGreater' | // Input is a string/number, needs to equal or greater than another number field 'numberEqualLess' | // Input is a string/number, needs to equal or less than another number field 'andOr', // Input is a string/number, needs to evaluate on any of the supplied statements contained apiField?: string, // Used if the widgetField name is different from the field in database andOr?: Omit<SearchObject, 'andOr'>[] } export type IdxEmitter = (source: IdxComponentScope, var1?: any, var2?: any, var3?: any) => any export type IdxEmitterInit = IdxEmitter & ((source: IdxComponentScope) => any) export type IdxEmitterSessionInit = IdxEmitter & ((source: null) => any) export type IdxEmitterCollectionUpdated = IdxEmitter & ((source: IdxListScope, collection?: Collection) => any) export type IdxEmitterPageChanged = IdxEmitter & ((source: IdxListScope, pageNumber?: number) => any) export type IdxEmitterPageChanging = IdxEmitter & ((source: IdxListScope, pageNumber?: number) => any) export type IdxEmitterOrderChanged = IdxEmitter & ((source: IdxListScope, order?: string | string[]) => any) export type IdxEmitterOrderChanging = IdxEmitter & ((source: IdxListScope, order?: string | string[]) => any) export type IdxEmitterSearching = IdxEmitter & ((source: IdxComponentScope, query?: CompileFilterOptions) => any) export type IdxEmitterSearched = IdxEmitter & ((source: IdxComponentScope, query?: CompileFilterOptions) => any) // All Service functionality const angularJsService = ( $http: IHttpService, $mdToast: material.IToastService, $q: IQService, $rootScope: IRootScopeService, $window: IWindowService, ListTrac: any, orderByFilter: IFilterOrderBy ): IdxService => { const sharedValues: IdxSharedValue = { contactUrl: null, contactCommentVariable: null, contact: null, integrations: { analytics: {}, maps: {} } } // Blank options to initialize arrays const defaultWhereOptions: WhereOptions = { City: [], // Added as default so search and manipulate eCity: [], // Added as default so search and manipulate UnparsedAddress: [], // Added as default so search and manipulate StreetAddress: [], // Added as default so search and manipulate eStreetAddress: [], // Added as default so search and manipulate Location: '', // Added as default so search and manipulate eLocation: '', // Added as default so search and manipulate Status: [], ListingId: [], ListingType: [], CountyOrParish: [], eCountyOrParish: [], MLSAreaMajor: [], eMLSAreaMajor: [], MLSAreaMinor: [], eMLSAreaMinor: [], SubdivisionName: [], eSubdivisionName: [], Neighborhood: [], eNeighborhood: [], PostalCode: [], // NOTE: at this point we don't know if CityRegion is used (or how it differs from MLSAreaMajor) CityRegion: [], eCityRegion: [], AgentLicense: [], OfficeNumber: [], OfficeName: [] } let idxServicesEnabled: number[] = [] let tokenRefreshURL = '/ajax/request?class=property.token_auth&method=getToken' let sessionInitialized = false let refreshLoginTimer: any // Timeout object let defaultPageTitle: string const instance: { disclaimer: { [uid: string]: IdxDisclaimerScope } map: { [uid: string]: IdxMapScope } member: { details: { [uid: string]: IdxDetailsScope } list: { [uid: string]: IdxListScope } search: { [uid: string]: IdxSearchScope } } office: { details: { [uid: string]: IdxDetailsScope } list: { [uid: string]: IdxListScope } search: { [uid: string]: IdxSearchScope } } property: { details: { [uid: string]: IdxDetailsScope } list: { [uid: string]: IdxListScope } search: { [uid: string]: IdxSearchScope } } } = { disclaimer: {}, map: {}, member: { details: {}, list: {}, search: {} }, office: { details: {}, list: {}, search: {} }, property: { details: {}, list: {}, search: {} } } /** type {{List: Object<[String]>, Search: Object<[String]>}} */ const instanceLink: { Disclaimer: { [uid: string]: string[] } List: { [uid: string]: string[] } Map: { [uid: string]: string[] } Search: { [uid: string]: string[] } } = { Disclaimer: {}, List: {}, Map: {}, Search: {} } const instanceOnEmitters: { [emitterUid: string]: { // [onMethodName: string]: IdxEmitter[] [onMethodName: string]: {[uid: string]: IdxEmitter} init?: {[uid: string]: IdxEmitterInit} sessionInit?: {[uid: string]: IdxEmitterSessionInit} collectionUpdated?: {[uid: string]: IdxEmitterCollectionUpdated} pageChanged?: {[uid: string]: IdxEmitterPageChanged} pageChanging?: {[uid: string]: IdxEmitterPageChanging} orderChanged?: {[uid: string]: IdxEmitterOrderChanged} orderChanging?: {[uid: string]: IdxEmitterOrderChanging} searched?: {[uid: string]: IdxEmitterSearched} searching?: {[uid: string]: IdxEmitterSearching} } } = { /*idx_property_list_7: { somethingChangedFake: [ (source) => { console.log('The something updated in this scope', source) } ], collectionUpdated: [ (source: IdxComponentScope, collection: Collection) => { console.log('The collection in this scope updated', source) console.log('collection is now', collection) } ] }*/ } /** type {{services: Array<MLSService>, lastCreated: Date, lastTtl: number}} */ const session: Session = { services: [], lastCreated: new Date(), lastTtl: 0, contacts: [] } const urlOptions: UrlsOptionsObject = { Listing: {}, Search: {} } /** * The last where query that was sent we're holding on to. This is mostly so we can move from page to page properly. * type {{whereFilter: {}, pages: Array<Number>, perPage: number}} */ const lastQueries: { whereFilter: object | any pages: number[] perPage: number order?: string | string[] time?: Date } = { whereFilter: {}, pages: [], perPage: 0, time: null } // TODO infer the emit type function emit( emitterName: string, $scope: IdxComponentScope, var1?: any, var2?: any, var3?: any ) { emitManual(emitterName, $scope.elementId, $scope, var1, var2, var3) } function emitManual( emitterName: string, uid: string, $scope: IdxComponentScope, var1?: any, var2?: any, var3?: any ) { if ( Object.prototype.hasOwnProperty.call(instanceOnEmitters, uid) && Object.prototype.hasOwnProperty.call(instanceOnEmitters[uid], emitterName) ) { Object.values(instanceOnEmitters[uid][emitterName]).forEach((emitter) => { try { emitter($scope, var1, var2, var3) } catch (e) { console.error(e, 'issue sending back emitter on', uid, emitterName, emitter) } }) } if (emitterName === 'init') { // Let's prep the requests for 'init' so they immediate call if this scope has already init instanceOnEmitters[uid] ??= {} instanceOnEmitters[uid][emitterName] ??= {} } } /** * Removes a registered on emitter watcher * @param emitterName - Id of on scope requesting event updates * @param emitterId - The source of emitting id * @param onId - Id of on scope requesting event updates */ function removeOnManual( emitterName: string, emitterId: string, onId: string, ) { if ( Object.prototype.hasOwnProperty.call(instanceOnEmitters, emitterId) && Object.prototype.hasOwnProperty.call(instanceOnEmitters[emitterId], emitterName) && Object.prototype.hasOwnProperty.call(instanceOnEmitters[emitterId][emitterName], onId) ) { delete instanceOnEmitters[emitterId][emitterName][onId] } } // TODO infer the emit type function on( uid: string, emitterName: string, callback: IdxEmitter ) { // console.log('a request has been made to watch for', uid, 'to emit', emitterName) // Let's check if an init request has already missed it's opportunity to init if ( emitterName === 'init' && Object.prototype.hasOwnProperty.call(instanceOnEmitters, uid) && Object.prototype.hasOwnProperty.call(instanceOnEmitters[uid], emitterName) && Object.prototype.hasOwnProperty.call(Stratus.Instances, uid) ) { // init has already happened.... so let's send back the emit of 'init' right now! // emit('init', Stratus.Instances[uid]) // wait, would this send the init a second time? maybe just send it to this callback callback(Stratus.Instances[uid]) return } if ( uid === 'Idx' && emitterName === 'sessionInit' && sessionInitialized ) { // sessionInitialized has already happened.... so let's send back the emit of 'sessionInitialized' right now! callback(null) return } if (!Object.prototype.hasOwnProperty.call(instanceOnEmitters, uid)) { instanceOnEmitters[uid] = {} } if (!Object.prototype.hasOwnProperty.call(instanceOnEmitters[uid], emitterName)) { instanceOnEmitters[uid][emitterName] = {} } const onId = uniqueId() // TODO make a named connection to the requesting scope?? instanceOnEmitters[uid][emitterName][onId] = callback return (): void => {removeOnManual(uid, emitterName, onId)} } /** * Add Search instance to the service * @param uid - The elementId of a widget * @param $scope - angular scope * @param moduleName - Property / Member / Office */ function registerDetailsInstance( uid: string, moduleName: 'member' | 'office' | 'property', $scope: IdxDetailsScope, ): void { instance[moduleName].details[uid] = $scope } /** * Add List instance to the service * @param uid - The elementId of a widget * @param $scope - angular scope * @param moduleName - Property / Member / Office */ function registerListInstance( uid: string, moduleName: 'member' | 'office' | 'property', $scope: IdxListScope ): void { if (!Object.prototype.hasOwnProperty.call(instance, moduleName)) { instance[moduleName].list = {} } instance[moduleName].list[uid] = $scope if (!Object.prototype.hasOwnProperty.call(instanceLink.List, uid)) { instanceLink.List[uid] = [] } } /** * Add Search instance to the service, potentially connecting to a List * @param uid - The elementId of a widget * @param $scope - angular scope * @param listUid - uid name * @param moduleName - Property / Member / Office */ function registerSearchInstance( uid: string, moduleName: 'member' | 'office' | 'property', $scope: IdxSearchScope, listUid?: string ): void { instance[moduleName].search[uid] = $scope if (!Object.prototype.hasOwnProperty.call(instanceLink.Search, uid)) { instanceLink.Search[uid] = [] } if (listUid) { // console.log('added', uid, 'to', listUid, 'instanceLink') instanceLink.Search[uid].push(listUid) if (!Object.prototype.hasOwnProperty.call(instanceLink.List, listUid)) { instanceLink.List[listUid] = [] } instanceLink.List[listUid].push(uid) } } /** * Destroy a reference and Instance of a Details widget * @param uid - The elementId of a widget * @param moduleName - Property / Member / Office */ function unregisterDetailsInstance( uid: string, moduleName: 'member' | 'office' | 'property' ): void { if (Object.prototype.hasOwnProperty.call(instance[moduleName].details, uid)) { const detailUid = instance[moduleName].details[uid].elementId delete instance[moduleName].details[uid] Stratus.Instances.Clean(detailUid) } } /** * Add Map instance to the service * @param uid - The elementId of a widget * @param $scope - angular scope */ function registerMapInstance(uid: string, $scope: IdxMapScope): void { if (!Object.prototype.hasOwnProperty.call(instance, 'map')) { instance.map = {} } instance.map[uid] = $scope if (!Object.prototype.hasOwnProperty.call(instanceLink.Map, uid)) { instanceLink.Map[uid] = [] } } /** * Add Disclaimer instance to the service * @param uid - The elementId of a widget * @param $scope - angular scope */ function registerDisclaimerInstance(uid: string, $scope: IdxDisclaimerScope): void { if (!Object.prototype.hasOwnProperty.call(instance, 'disclaimer')) { instance.disclaimer = {} } instance.disclaimer[uid] = $scope if (!Object.prototype.hasOwnProperty.call(instanceLink.Disclaimer, uid)) { instanceLink.Disclaimer[uid] = [] } } /** * Return Blank options to initialize arrays */ function getDefaultWhereOptions(): WhereOptions { return clone(defaultWhereOptions) } /** * Return the List scopes of a those connected to a particular Search widget * @param searchUid - uid of search component * @param moduleName - Property / Member / Office * FIXME only using IdxComponentScope until all converted */ function getSearchInstanceLinks( searchUid: string, moduleName: 'member' | 'office' | 'property' = 'property' ): (IdxPropertyListScope | IdxComponentScope)[] { const linkedLists: (IdxPropertyListScope | IdxComponentScope)[] = [] if (Object.prototype.hasOwnProperty.call(instanceLink.Search, searchUid)) { instanceLink.Search[searchUid].forEach((listUid: any) => { if (Object.prototype.hasOwnProperty.call(instance[moduleName].list, listUid)) { linkedLists.push(instance[moduleName].list[listUid]) } }) } return linkedLists } /** * Return a List scope * @param listUid - uid of List * @param moduleName - Property / Member / Office * FIXME only using IdxComponentScope until all converted */ function getListInstance( listUid: string, moduleName: 'member' | 'office' | 'property' = 'property' ): IdxPropertyListScope | IdxComponentScope | null { return Object.prototype.hasOwnProperty.call(instanceLink.List, listUid) ? instance[moduleName].list[listUid] : null } /** * Return the Search scopes of a those connected to a particular List widget * @param listUid - uid of list * @param moduleName - Property / Member / Office * FIXME only using IdxComponentScope until all converted */ function getListInstanceLinks( listUid: string, moduleName: 'member' | 'office' | 'property' = 'property' ): (IdxPropertySearchScope | IdxMapScope | IdxComponentScope)[] { const linkedSearches: (IdxPropertySearchScope | IdxComponentScope)[] = [] if (Object.prototype.hasOwnProperty.call(instanceLink.List, listUid)) { instanceLink.List[listUid].forEach((searchUid) => { if (Object.prototype.hasOwnProperty.call(instance[moduleName].list, searchUid)) { linkedSearches.push(instance[moduleName].list[searchUid]) } }) } return linkedSearches } /** * Return a Disclaimer scope * @param disclaimerUid - uid of Disclaimer */ function getDisclaimerInstance(disclaimerUid?: string): {[uid: string]: IdxDisclaimerScope} | null { return disclaimerUid ? ( Object.prototype.hasOwnProperty.call(instance.disclaimer, disclaimerUid) ? {[instance.disclaimer[disclaimerUid].elementId]: instance.disclaimer[disclaimerUid]} : null ) : instance.disclaimer } /** * Apply a new Page title or revert to the original; page title * @param title - Page Title */ function setPageTitle(title?: string): void { if (!defaultPageTitle) { // save default title first defaultPageTitle = JSON.parse(JSON.stringify($window.document.title)) } if (!title) { // Revert to default $window.document.title = defaultPageTitle } else { // Apply new title $window.document.title = title } } /** * Retrieve what services that we are fetching tokens for */ function getIdxServices(): number[] { return idxServicesEnabled } /** * Updates what services that we are fetching tokens for * @param services = services to fetches the tokens and search listings for */ function setIdxServices(services: number[]): void { idxServicesEnabled = services } /** * Updates the token url to another path * @param url = URL to change to */ function setTokenURL(url: string): void { tokenRefreshURL = url } /** * Ensures there is a active session and performs token fetching if need be. * @param keepAlive - */ function tokenKeepAuth(keepAlive = false): IPromise<void> { return $q(async (resolve: void | any, reject: any) => { try { if ( Object.keys(session.services).length < 1 || session.expires < new Date(Date.now() + (5 * 1000)) // if expiring in the next 5 seconds ) { // need to send ?cacheReset=true to ensure the token is new await tokenRefresh(keepAlive, true) resolve() } else { resolve() } } catch (err) { console.error('tokenKeepAuth Error:', err) reject(err) } }) } /** * Fetches a new set of Tokens for data fetching * @param keepAlive - * @param cacheReset - if the cache needs to forcibly be reset and ensure this is a fresh token */ function tokenRefresh(keepAlive: boolean = false, cacheReset: boolean = false): IPromise<void> { // TODO request only certain service_ids (&service_id=0,1,9 or &service_id[]=0&service_id[]=9) return $q((resolve: void | any, reject: void | any) => { let additionalQueries = `&apiVersion=${apiVersion}` // Fetch from each service allowed if (idxServicesEnabled.length !== 0) { idxServicesEnabled.forEach((service) => { additionalQueries += `&service[]=${service}` }) } if (cacheReset) { additionalQueries += '&cacheReset=true' } $http({ method: 'GET', url: tokenRefreshURL + additionalQueries }).then((response: TokenResponse | any) => { // response as TokenResponse if ( typeof response === 'object' && Object.prototype.hasOwnProperty.call(response, 'data') && Object.prototype.hasOwnProperty.call(response.data, 'services') && Object.prototype.hasOwnProperty.call(response.data.services, 'length') ) { tokenHandleGoodResponse(response, keepAlive) resolve() } else { reject(tokenHandleBadResponse(response)) } }, (response: TokenResponse) => { // TODO interface a response reject(tokenHandleBadResponse(response)) }) }) } /** * Functions to do once successfully retrieve a new set of tokens. * Currently will set a timer to refresh tokens after XXX * @param response - valid token response * @param keepAlive - * @param response.data.services Array<MLSService> */ function tokenHandleGoodResponse(response: TokenResponse, keepAlive = false): void { session.services = {} /** {MLSService} service */ response.data.services.forEach((service) => { if (Object.prototype.hasOwnProperty.call(service, 'id')) { if (!Object.prototype.hasOwnProperty.call(service, 'fetchTime')) { service.fetchTime = { Property: null, Media: null, Member: null, Office: null, OpenHouse: null } } if ( !Object.prototype.hasOwnProperty.call(service, 'analyticsEnabled') || service.analyticsEnabled === null ) { service.analyticsEnabled = [] } if ( !Object.prototype.hasOwnProperty.call(service, 'logo') || service.logo === null ) { service.logo = { default: null } } if ( !Object.prototype.hasOwnProperty.call(service, 'mandatoryLogo') || service.mandatoryLogo === null ) { service.mandatoryLogo = [] } session.services[service.id] = service session.lastCreated = new Date(service.created)// The object is a String being converted to Date session.lastTtl = service.ttl // Set to expire 15 secs before it actually does session.expires = new Date(session.lastCreated.getTime() + (session.lastTtl - 15) * 1000) } }) // FIXME prevent more than a single service from populating. prefer the service other than 0 /*if (session.services.length > 1) { let singleService = session.services.pop() if (singleService.id === 0) { // If this is the exclusive... try once again singleService = session.services.pop() } session.services = [] session.services[singleService.id] = singleService }*/ // Compile a contact from the response if it exists if ( Object.prototype.hasOwnProperty.call(response.data, 'contactUrl') && response.data.contactUrl !== '' ) { sharedValues.contactUrl = response.data.contactUrl } if ( Object.prototype.hasOwnProperty.call(response.data, 'contactCommentVariable') && response.data.contactCommentVariable !== '' ) { sharedValues.contactCommentVariable = response.data.contactCommentVariable } // Compile a contact from the response if it exists if (Object.prototype.hasOwnProperty.call(response.data, 'contact')) { sharedValues.contact = { name: '', emails: {}, locations: {}, phones: {}, socialUrls: {}, urls: {}, } if ( Object.prototype.hasOwnProperty.call(response.data, 'site') && isString(response.data.site) && response.data.site !== '' ) { sharedValues.contact.name = response.data.site } if ( Object.prototype.hasOwnProperty.call(response.data, 'contactName') && isString(response.data.contactName) && response.data.site !== '' ) { sharedValues.contact.name = response.data.contactName } if ( Object.prototype.hasOwnProperty.call(response.data, 'contact') && isPlainObject(response.data.contact) ) { if ( Object.prototype.hasOwnProperty.call(response.data.contact, 'emails') && isPlainObject(response.data.contact.emails) ) { sharedValues.contact.emails = response.data.contact.emails } if ( Object.prototype.hasOwnProperty.call(response.data.contact, 'locations') && isPlainObject(response.data.contact.locations) ) { sharedValues.contact.locations = response.data.contact.locations } if ( Object.prototype.hasOwnProperty.call(response.data.contact, 'phones') && isPlainObject(response.data.contact.phones) ) { sharedValues.contact.phones = response.data.contact.phones } if ( Object.prototype.hasOwnProperty.call(response.data.contact, 'socialUrls') && isPlainObject(response.data.contact.socialUrls) ) { sharedValues.contact.socialUrls = response.data.contact.socialUrls } if ( Object.prototype.hasOwnProperty.call(response.data.contact, 'urls') && isPlainObject(response.data.contact.urls) ) { sharedValues.contact.urls = response.data.contact.urls } } } // Compile a contact from the response if it exists if (Object.prototype.hasOwnProperty.call(response.data, 'integrations')) { if (Object.prototype.hasOwnProperty.call(response.data.integrations, 'analytics')) { if (Object.prototype.hasOwnProperty.call(response.data.integrations.analytics, 'googleAnalytics')) { if ( Object.prototype.hasOwnProperty.call(response.data.integrations.analytics.googleAnalytics, 'accountId') && isString(response.data.integrations.analytics.googleAnalytics.accountId) && response.data.integrations.analytics.googleAnalytics.accountId !== '' ) { sharedValues.integrations.analytics.googleAnalytics = { accountId: response.data.integrations.analytics.googleAnalytics.accountId } } } if (Object.prototype.hasOwnProperty.call(response.data.integrations.analytics, 'listTrac')) { if ( Object.prototype.hasOwnProperty.call( response.data.integrations.analytics.listTrac, 'accountId' ) && isString(response.data.integrations.analytics.listTrac.accountId) && response.data.integrations.analytics.listTrac.accountId !== '' ) { sharedValues.integrations.analytics.listTrac = { accountId: response.data.integrations.analytics.listTrac.accountId } ListTrac.setAccountId(sharedValues.integrations.analytics.listTrac.accountId) // FIXME we only need to load ListTrac/send an event when the the MLS is whitelisted for it } } } if (Object.prototype.hasOwnProperty.call(response.data.integrations, 'maps')) { if (Object.prototype.hasOwnProperty.call(response.data.integrations.maps, 'googleMaps')) { if ( Object.prototype.hasOwnProperty.call(response.data.integrations.maps.googleMaps, 'publicKey') && isString(response.data.integrations.maps.googleMaps.publicKey) && response.data.integrations.maps.googleMaps.publicKey !== '' ) { sharedValues.integrations.maps.googleMaps = { publicKey: response.data.integrations.maps.googleMaps.publicKey } } } } } if (!sessionInitialized) { emitManual('sessionInit', 'Idx', null) } emitManual('sessionRefresh', 'Idx', null) sessionInitialized = true if (keepAlive) { tokenEnableRefreshTimer() } } /** * Functions to do if the token retrieval fails. For now it just outputs the errors * @param response - */ function tokenHandleBadResponse(response: TokenResponse | any): string | any { let errorMessage: any = 'Token supplied is invalid or blank' if ( typeof response === 'object' && Object.prototype.hasOwnProperty.call(response, 'data') && Object.prototype.hasOwnProperty.call(response.data, 'errors') && Object.prototype.hasOwnProperty.call(response.data.errors, 'length') ) { // console.error('Token Error', response.data.errors) errorMessage = response.data.errors } else { // console.error('Token Error', response) } $mdToast.show( $mdToast.simple() .textContent('Unable to authorize Idx feed!') .toastClass('errorMessage') .position('top right') .hideDelay(5000) ) return errorMessage } /** * Set a timer to attempt to run a token fetch again 15 secs before the current tokens expire */ function tokenEnableRefreshTimer(): void { clearTimeout(refreshLoginTimer) refreshLoginTimer = setTimeout(async () => { await tokenRefresh() }, (session.lastTtl - 15) * 1000) // 15 seconds before the token expires } function updateFetchTime( apiFetch: Collection | Model, modelName: 'Property' | 'Media' | 'Member' | 'Office' | 'OpenHouse', serviceId: number): void { // TODO save collection.header.get('x-fetch-time') to MLSVariables const fetchTime = apiFetch.header.get('x-fetch-time') if (fetchTime) { const oldTime = session.services[serviceId].fetchTime[modelName] session.services[serviceId].fetchTime[modelName] = new Date(fetchTime) // TODO check differences or old vs new and push emit if ( !(isDate(oldTime) && isDate(session.services[serviceId].fetchTime[modelName])) || oldTime.getTime() !== session.services[serviceId].fetchTime[modelName].getTime() ) { // Only emit if there is a new time set emitManual('fetchTimeUpdate', 'Idx', null, serviceId, modelName, session.services[serviceId].fetchTime[modelName]) } } } /** * Model constructor helper that will help properly create a new Model. * Will do nothing else. * @param request - Standard Registry request object * TODO define type Request */ function creat