@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
text/typescript
/**
* @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