@stratusjs/idx
Version:
AngularJS idx/property Service and Components bundle to be used as an add on to StratusJS
974 lines (903 loc) • 43.5 kB
text/typescript
/**
* @file IdxPropertySearch Component @stratusjs/idx/property/search.component
* @example <stratus-idx-property-search>
* @see https://github.com/Sitetheory/stratus/wiki/Idx-Property-Search-Widget
*/
// Compile Stylesheets
import './search.component.less'
import './search.compact.component.less'
import './search.classic.component.less'
// Runtime
import _, {
clone,
cloneDeep,
compact,
extend,
includes,
intersection,
isArray,
isEmpty,
isEqual,
isNumber,
isString,
isUndefined,
map,
throttle,
union
} from 'lodash'
import {Stratus} from '@stratusjs/runtime/stratus'
import {element, material, IAttributes, ITimeoutService, IQService, IWindowService, ISCEService, IAugmentedJQuery} from 'angular'
import 'angular-material'
import {
CompileFilterOptions,
IdxComponentScope,
IdxEmitter,
IdxSearchScope,
IdxService,
MLSService,
SelectionGroup,
WhereOptions
} from '@stratusjs/idx/idx'
import {IdxPropertyListScope} from '@stratusjs/idx/property/list.component'
import {isJSON, LooseObject, safeUniqueId} from '@stratusjs/core/misc'
import {cookie} from '@stratusjs/core/environment'
// Stratus Preload
import '@stratusjs/angularjs-extras' // directives/stringToNumber + filters/numeral
type NameValuePair = {
name: string
value: string | number | string[]
}
// Environment
const min = !cookie('env') ? '.min' : ''
const packageName = 'idx'
const moduleName = 'property'
const componentName = 'search'
// There is not a very consistent way of pathing in Stratus at the moment
const localDir = `${Stratus.BaseUrl}${Stratus.DeploymentPath}@stratusjs/${packageName}/src/${moduleName}/`
const localDistStyle = `${Stratus.BaseUrl}${Stratus.DeploymentPath}@stratusjs/${packageName}/dist/${packageName}.bundle.min.css`
type ListingTypeSelectionSetting = {
name: string
value: string
group: string
lease: boolean
}
export type IdxPropertySearchScope = IdxSearchScope & {
widgetName: string
initialized: boolean
listId: string
listInitialized: boolean
listLinkUrl: string
// Can be 'simple' (location only), 'basic' (beds/baths, price, etc), 'advanced' (dropdown filters)
searchType: 'simple' | 'basic' | 'advanced'
// Href to an url
advancedSearchUrl: string
advancedSearchLinkName: string
advancedFiltersStatus: boolean
openPrice: boolean
listLinkTarget: string
// options: object | any // TODO need to specify
options: {
// [key: string]: object | any
query: CompileFilterOptions
selection: { // object | any // TODO need to specify
Bedrooms?: NameValuePair[]
Bathrooms?: NameValuePair[]
order?: NameValuePair[]
Status?: LooseObject<LooseObject<string[]>>
ListingType?: {
group?: {
[propertyCategory: string]: boolean
}
list?: {
Residential: string[]
Commercial: string[]
Lease: string[]
}
default?: {
Sale: {
[propertyCategory: string]: string[]
}
Lease: {
[propertyCategory: string]: string[]
}
}
All?: ListingTypeSelectionSetting[]
}
}
forRent: boolean
agentGroups: SelectionGroup[]
officeGroups: SelectionGroup[]
}
presetLocationText: string // Will be formed based on the original query where
presetLocationHTML: any // Will be formed based on the original query where
presetOtherFiltersCountText?: string // Will be formed based on the original query Agent, Office Group, and Listing ID (with + text)
presetLocationsCountText?: string // Number of Location filters in use (with + text)
presetLocationsRemainingCountText?: string // Number of Location filters in use minus 2 (with + text)
displayFilterFullHeight: boolean
variableSyncing: object | any
filterMenu?: material.IPanelRef & any // material.IPanelRef // disabled because we need to set reposition()
_: typeof _
// Functions
canDisplayListingTypeButton(listType: ListingTypeSelectionSetting): boolean
displayOfficeGroupSelector(searchTerm?: string, editIndex?: number, ev?: any): void
focusElement(selector: string): void
getMLSVariables(reset?: boolean): MLSService[]
getOtherPresetFilterCount(): number
getPresetLocations(): string[]
hasQueryChanged(): boolean
inArray(item: any, array: any[]): boolean
isIntersecting(itemArray: any[], array: any[]): boolean
isPresetLocationSet(): boolean
resetLocationQuery(): void
parsePresetLocationText(): void
selectDefaultListingType(listingGroup?: string): void
setQuery(newQuery?: CompileFilterOptions): void
setWhere(newWhere?: WhereOptions): void
setWhereDefaults(): void
showInlinePopup(ev: any, menuElement: string): void
throttledSearch(): void
toggleArrayElement(item: any, array: any[]): void
variableSync(): Promise<void>
}
Stratus.Components.IdxPropertySearch = {
/** @see https://github.com/Sitetheory/stratus/wiki/Idx-Property-Search-Widget#Widget_Parameters */
bindings: {
// TODO doc
elementId: '@',
/**
* Type: boolean
* Two-way bound option. When needing to provide data/options to this widget in an async environment, this
* initialization can be delayed by supplying a bound variable to notify when the data is ready.
* @example `init-now="model.completed"`
*/
initNow: '=',
/**
* Type: string
* To determine what datasource the Idx widgets are able to pull from and their temporary credentials, a
* `token-url` directs to the location that manages this widget's subscription and provides what Idx servers it
* has access to.
*/
tokenUrl: '@',
// TODO doc
tokenOnLoad: '@',
/**
* Type: string
* ID of Property List widget to attach and control. The counterpart Property List widget's `element-id` must be
* defined and the same as this `list-id` (See Property List). Multiple Search widgets may attach to the same
* List widget but, a Search widget may only control a single List widget.
*/
listId: '@',
/**
* Type: string
* Default: '/property/list'
* If a List widget does not share the same page as this Search widget, or they are not connected via
* `list-id`/`element-id`, searching will instead load a new page to where ever the List widget may be found
* (Url provided here).
*/
listLinkUrl: '@',
/**
* Type: string
* Default: '_self'
* Combined with `list-link-url`, if ever searching on another page, this will define how to open the page as a
* normal link. Any usable HTML <b>'_target'</b> attribute such as '_self' or '_blank'.
*/
listLinkTarget: '@',
/**
* Type: string
* Default: 'advanced'
* Options: 'simple' (only a location field), 'basic' (location plus beds/baths/price), 'advanced' (everything)
*/
searchType: '@',
/**
* Type: string
* A link to another dedicated advanced search page (used when this is a module). NOTE: this should generally be
* the same as linkListUrl and if not set it will set this to match.
*/
advancedSearchUrl: '@',
/**
* Type: string
* Default: 'Advanced Search'
* An alternative name for the advanced search button.
*/
advancedSearchLinkName: '@',
/**
* Type: string
* Default: 'search'
* The file name in which is loaded for the view of the widget. The name will automatically be appended with
* '.component.min.html'. The default is 'search.component.html'.
* @TODO Will need to allow setting a custom path of views outside the library directory.
*/
template: '@',
/**
* Type: json
* Additional advanced parameters that may control what the Search interface displays. Only parameter used at
* this time is selection
* @TODO
*/
options: '@',
/** Type: SelectionGroup[] */
optionsAgentGroups: '@',
/** Type: SelectionGroup[] */
optionsOfficeGroups: '@',
/**
* Type: boolean
* Whether to ensure the Filter menu is full height. Only Available if template allows and set to advanced
* @TODO add to wiki
*/
displayFilterFullHeight: '@',
// TODO
variableSync: '@',
// TODO
widgetName: '@'
},
controller(
$attrs: IAttributes,
$element: IAugmentedJQuery,
$q: IQService,
$mdConstant: any, // mdChips item
$mdDialog: material.IDialogService,
$mdPanel: material.IPanelService,
$sce: ISCEService,
$scope: IdxPropertySearchScope,
$timeout: ITimeoutService,
$window: IWindowService,
Idx: IdxService,
) {
// Initialize
$scope.uid = safeUniqueId(packageName, moduleName, componentName)
$scope.elementId = $attrs.elementId || $scope.uid
$scope._ = _
Stratus.Instances[$scope.elementId] = $scope
$scope.localDir = localDir
$scope.initialized = false
if ($attrs.tokenUrl) {
Idx.setTokenURL($attrs.tokenUrl)
}
Stratus.Internals.CssLoader(localDistStyle).then()
// Default values
let defaultQuery: LooseObject
let lastQuery: CompileFilterOptions
let mlsVariables: {[serviceId: number]: MLSService}
$scope.openPrice = false
$scope.advancedFiltersStatus = false
$scope.advancedSearchUrl = ''
$scope.advancedSearchLinkName = 'Advanced Search'
$scope.presetLocationText = ''
// Used by template
$scope.$mdConstant = $mdConstant
/**
* All actions that happen first when the component loads
* Need to be placed in a function, as the functions below need to the initialized first
*/
const init = async () => {
$scope.widgetName = $attrs.widgetName || ''
$scope.listId = $attrs.listId || null
$scope.listInitialized = false
$scope.listLinkUrl = $attrs.listLinkUrl || $attrs.advancedSearchUrl || '/property/list'
$scope.listLinkTarget = $attrs.listLinkTarget || '_self'
$scope.searchType = $attrs.searchType || 'advanced'
// NOTE: this does not default to listLinkUrl in case they don't want an advanced search button they leave blank
$scope.advancedSearchUrl = $attrs.advancedSearchUrl || $scope.advancedSearchUrl
$scope.advancedSearchLinkName = $attrs.advancedSearchLinkName || $scope.advancedSearchLinkName
$scope.options = $attrs.options && isJSON($attrs.options) ? JSON.parse($attrs.options) : {}
$scope.displayFilterFullHeight = $attrs.displayFilterFullHeight && isJSON($attrs.displayFilterFullHeight) ?
JSON.parse($attrs.displayFilterFullHeight) : false
$scope.filterMenu = null
$scope.options.forRent ||= false
$scope.options.agentGroups ??= []
// $scope.options.officeGroups ??= []
$scope.options.officeGroups =
($scope.options.officeGroups && isString($scope.options.officeGroups) && isJSON($scope.options.officeGroups)
? JSON.parse($scope.options.officeGroups)
: ($attrs.optionsOfficeGroups && isJSON($attrs.optionsOfficeGroups)
? JSON.parse($attrs.optionsOfficeGroups)
: $scope.options.officeGroups)
) || []
// Set default queries
$scope.options.query ??= {}
$scope.options.query.where ??= {}
$scope.options.query.service ??= []
// $scope.setQuery($scope.options.query)
$scope.setWhere($scope.options.query.where)
defaultQuery = JSON.parse(JSON.stringify(cloneDeep($scope.options.query.where)))
if ($scope.options.query.order) {
defaultQuery.Order = $scope.options.query.order
}
// console.log('$scope.options.query is starting at ', clone($scope.options.query))
// If the List hasn't updated this widget after 1 second, make sure it's checked again. A workaround for
// the race condition for now, up for suggestions
$timeout(async () => {
/*if (!$scope.listInitialized) {
await $scope.refreshSearchWidgetOptions()
}*/
// Sync needs to happen here so that the List and still connect with the Search widget
await $scope.variableSync()
}, 1000)
// Set default selections TODO may need some more universally set options to be able to use
$scope.options.selection ??= {}
$scope.options.selection.Bedrooms ??= [
{name: '1+', value: 1},
{name: '2+', value: 2},
{name: '3+', value: 3},
{name: '4+', value: 4},
{name: '5+', value: 5}
]
$scope.options.selection.Bathrooms ??= [
{name: '1+', value: 1},
{name: '2+', value: 2},
{name: '3+', value: 3},
{name: '4+', value: 4},
{name: '5+', value: 5}
]
$scope.options.selection.order ??= [
{name: 'Highest Price', value: '-BestPrice'},
{name: 'Lowest Price', value: 'BestPrice'},
{name: 'Recently Updated', value: '-ModificationTimestamp'},
{name: 'Recently Sold', value: '-CloseDate'},
{name: 'Status + Price', value: ['Status', '-BestPrice']},
{name: 'Status + Recent', value: ['Status', '-ModificationTimestamp']}
]
$scope.options.selection.Status ??= {}
$scope.options.selection.Status.default ??= {
Sale: ['Active', 'Contract'],
Lease: ['Active']
}
$scope.options.selection.ListingType ??= {}
// These determine what ListingTypes options that should currently be 'shown' based on selections.
// Automatically updated with a watcher
$scope.options.selection.ListingType.group ??= {
Residential: true,
Commercial: false
}
// TODO These values need to be supplied by the MLS' to ensure we dont show ones that don't exist
$scope.options.selection.ListingType.list ??= {
Residential: ['House', 'Condo', 'Townhouse', 'MultiFamily', 'Manufactured', 'Land', 'LeaseHouse', 'LeaseCondo', 'LeaseTownhouse', 'LeaseOther'],
Commercial: ['Commercial', 'CommercialBusinessOp', 'CommercialResidential', 'CommercialLand', 'LeaseCommercial'],
Lease: ['LeaseHouse', 'LeaseCondo', 'LeaseTownhouse', 'LeaseOther', 'LeaseCommercial']
}
// These are the default selections and should be updated by the page on load(if needed)
$scope.options.selection.ListingType.default ??= {
Sale: {
Residential: ['House', 'Condo', 'Townhouse'],
Commercial: ['Commercial', 'CommercialBusinessOp']
},
Lease: {
Residential: ['LeaseHouse', 'LeaseCondo', 'LeaseTownhouse'],
Commercial: ['LeaseCommercial']
}
}
// These are static and never change. merely map correct values
$scope.options.selection.ListingType.All ??= [
{name: 'House', value: 'House', group: 'Residential', lease: false},
{name: 'Condo', value: 'Condo', group: 'Residential', lease: false},
{name: 'Townhouse', value: 'Townhouse', group: 'Residential', lease: false},
{name: 'Multi-Family', value: 'MultiFamily', group: 'Residential', lease: false},
{name: 'Manufactured', value: 'Manufactured', group: 'Residential', lease: false},
{name: 'Land', value: 'Land', group: 'Residential', lease: false},
{name: 'Other', value: 'Other', group: 'Residential', lease: false},
{name: 'Commercial', value: 'Commercial', group: 'Commercial', lease: false},
{name: 'Commercial Business Op', value: 'CommercialBusinessOp', group: 'Commercial', lease: false},
{name: 'Commercial Residential', value: 'CommercialResidential', group: 'Commercial', lease: false},
{name: 'Commercial Land', value: 'CommercialLand', group: 'Commercial', lease: false},
{name: 'House', value: 'LeaseHouse', group: 'Residential', lease: true},
{name: 'Condo', value: 'LeaseCondo', group: 'Residential', lease: true},
{name: 'Townhouse', value: 'LeaseTownhouse', group: 'Residential', lease: true},
{name: 'Other', value: 'LeaseOther', group: 'Residential', lease: true},
{name: 'Commercial', value: 'LeaseCommercial', group: 'Commercial', lease: true}
]
$scope.setWhereDefaults()
// Register this Search with the Property service
Idx.registerSearchInstance($scope.elementId, moduleName, $scope, $scope.listId) // May be deprecating
if ($scope.listId) {
// When the List loads, we need to update our settings with the list's
Idx.on($scope.listId, 'init', $scope.refreshSearchWidgetOptions)
Idx.on($scope.listId, 'searching', $scope.refreshSearchWidgetOptions)
}
// FIXME testing emitters
/*Idx.on($scope.listId, 'collectionUpdated', (source, collection: Collection) => {
console.log('collectionUpdated!!!!', source, collection)
})*/
if ($attrs.tokenOnLoad) {
try {
await Idx.tokenKeepAuth()
$scope.getMLSVariables(true)
} catch(e) {
console.error('Search is unable to load in token data', e)
}
}
$scope.$applyAsync(() => {
$scope.initialized = true
})
// await $scope.variableSync() sync is moved to teh timeout above, so it can still work with List widgets
Idx.emit('init', $scope)
}
// Initialization by Event
this.$onInit = () => {
$scope.Idx = Idx
let initNow = true
if (Object.prototype.hasOwnProperty.call($attrs.$attr, 'initNow')) {
// TODO: This needs better logic to determine what is acceptably initialized
initNow = isJSON($attrs.initNow) ? JSON.parse($attrs.initNow) : false
}
if (initNow) {
init().then()
return
}
const stopWatchingInitNow = $scope.$watch('$ctrl.initNow', (initNowCtrl: boolean) => {
// console.log('CAROUSEL initNow called later')
if (initNowCtrl !== true) {
return
}
if (!$scope.initialized) {
init().then()
}
stopWatchingInitNow()
})
}
$scope.$watch('options.query.where.ListingType', () => {
if ($scope.options?.query?.where && $scope.options?.selection?.ListingType?.list) {
if (!$scope.options?.query?.where?.ListingType) {
$scope.options.query.where.ListingType = []
}
if (!isArray($scope.options.query.where.ListingType)) {
$scope.options.query.where.ListingType = [$scope.options.query.where.ListingType]
}
$scope.options.selection.ListingType.group.Residential =
$scope.isIntersecting($scope.options.selection.ListingType.list.Residential, $scope.options.query.where.ListingType)
$scope.options.selection.ListingType.group.Commercial =
$scope.isIntersecting($scope.options.selection.ListingType.list.Commercial, $scope.options.query.where.ListingType)
$scope.options.forRent =
$scope.isIntersecting($scope.options.selection.ListingType.list.Lease, $scope.options.query.where.ListingType)
// console.log('watched ListingType', $scope.options.query.ListingType, $scope.options.selection.ListingType.group)
}
})
$scope.resetLocationQuery = (): void => {
$scope.options.query.where.Location = ''
$scope.options.query.where.eLocation = ''
$scope.options.query.where.UnparsedAddress = []
$scope.options.query.where.StreetAddress = []
$scope.options.query.where.eStreetAddress = []
$scope.options.query.where.City = []
$scope.options.query.where.eCity = []
$scope.options.query.where.CountyOrParish = []
$scope.options.query.where.eCountyOrParish = []
$scope.options.query.where.MLSAreaMajor = []
$scope.options.query.where.eMLSAreaMajor = []
$scope.options.query.where.Neighborhood = []
$scope.options.query.where.eNeighborhood = []
$scope.options.query.where.PostalCode = []
$scope.options.query.where.ListingId = []
// Reset Agent and license for the sake of user
$scope.options.query.where.AgentLicense = []
$scope.options.query.where.OfficeNumber = []
$scope.options.officeGroups = []
$scope.parsePresetLocationText()
}
$scope.getPresetLocations = (): string[] => {
const currentWhere = $scope.options.query.where
return compact(union(
currentWhere.UnparsedAddress,
currentWhere.StreetAddress,
currentWhere.eStreetAddress,
currentWhere.City,
currentWhere.eCity,
currentWhere.CountyOrParish,
currentWhere.eCountyOrParish,
currentWhere.MLSAreaMajor,
currentWhere.eMLSAreaMajor,
currentWhere.Neighborhood,
currentWhere.eNeighborhood,
currentWhere.PostalCode,
))
}
$scope.getOtherPresetFilterCount = (): number => {
const currentWhere = $scope.options.query.where
let filterCounts = compact(union(
currentWhere.OfficeNumber,
currentWhere.AgentLicense,
currentWhere.ListingId
)).length
if (currentWhere.OpenHouseOnly) {
filterCounts++
}
return filterCounts
}
$scope.isPresetLocationSet = (): boolean => {
return $scope.getPresetLocations().length > 0
}
$scope.parsePresetLocationText = (): void => {
const locations = $scope.getPresetLocations()
$scope.presetLocationText = locations.join(', ')
if (!isEmpty($scope.presetLocationText)) {
const html = `<span>${locations.join('</span><span>')}</span>`
$scope.presetLocationHTML = $sce.trustAsHtml(html)
// console.log('parsePresetLocationText forming html of', html, $scope.presetLocationHTML)
} else {
$scope.presetLocationHTML = null
}
$scope.presetLocationsCountText = null
$scope.presetLocationsRemainingCountText = null
// $scope.presetLocationsCountText = `+${locations.length} Locations${locations.length > 1 ? 's' : ''}`
// FIXME we may need a better controller to determine the number of locations to display... Bandaid for now
if (locations.length > 0) {
$scope.presetLocationsCountText = `+${locations.length}`
if (locations.length > 2) {
$scope.presetLocationsRemainingCountText = `+${locations.length - 2}`
}
}
$scope.presetOtherFiltersCountText = null
const filterCounts = $scope.getOtherPresetFilterCount()
if (filterCounts > 0) {
$scope.presetOtherFiltersCountText = `+${filterCounts} Filter${filterCounts > 1 ? 's' : ''}`
}
}
/** Focus a requested element. Only selectors elements within the component. */
$scope.focusElement = (selector: string): void => {
if (!isEmpty(selector)){
$timeout(() => {
const elementThis = $element[0]
const elementSelected = elementThis.querySelector(selector)
if (!elementSelected) {
console.warn('unable to find focusable element of', selector)
return
}
(elementSelected as any).focus()
}, 100) // Delay to allow angular to process first
}
}
/**
* Sync Gutensite form variables to a Stratus scope
* TODO move this to it's own directive/service
*/
$scope.variableSync = async (): Promise<void> => {
$scope.variableSyncing = $attrs.variableSync && isJSON($attrs.variableSync) ? JSON.parse($attrs.variableSync) : {}
// console.log('variables syncing: ', clone($scope.variableSyncing))
const promises: any[] = []
Object.keys($scope.variableSyncing).forEach((elementId: string) => {
promises.push(
$q(async (resolve: void | any) => {
const varElement = Idx.getInput(elementId)
if (varElement) {
// console.log('got input', varElement, clone(varElement.val()))
// Form Input exists
const scopeVarPath = $scope.variableSyncing[elementId]
// convert into a real var path and set the initial value from the exiting form value
await Idx.updateScopeValuePath($scope, scopeVarPath, varElement.val())
$scope.setWhere($scope.options.query.where) // ensure the basic items are always set
// Creating watcher to update the input when the scope changes
$scope.$watch(
scopeVarPath,
(value: any) => {
// console.log('detecting', scopeVarPath, 'as', value)
if (
isString(value) ||
isNumber(value) ||
isUndefined(value) ||
value == null
) {
if (isUndefined(value)) {
// elements can't process undefined... treat as null
value = null
}
// console.log('updating', scopeVarPath, 'value to', value, 'was', varElement.val())
varElement.val(value)
} else {
// console.log('updating json', scopeVarPath, 'value to', value, 'was', varElement.val())
varElement.val(JSON.stringify(value))
}
// varElement.fireEvent('onchange') // deprecated and no longer works
varElement[0].dispatchEvent(new Event('change'))
},
true
)
}
resolve()
})
)
})
await $q.all(promises)
}
$scope.canDisplayListingTypeButton = (listType: ListingTypeSelectionSetting): boolean => {
return $scope.options.forRent === listType.lease && $scope.options.selection.ListingType.group[listType.group]
}
$scope.inArray = (item: any, array: any[]) => includes(array, item)
$scope.isIntersecting = (itemArray: any[], array: any[]): boolean => {
if (!isArray(array) || !isArray(itemArray)) {
console.warn('Array undefined, cannot search for', itemArray, 'in', array)
// return []
return false
}
return intersection(itemArray, array).length > 0
}
/**
* Add or remove a certain element from an array
* TODO move to global reference
*/
$scope.toggleArrayElement = (item: any, array: any[]): void => {
array ??= []
const arrayIndex = array.indexOf(item)
if (arrayIndex >= 0) {
array.splice(arrayIndex, 1)
} else {
array.push(item)
}
}
/**
* Add a popup on screen using an existing element
* TODO could use more options
* @param ev - Click Event
* @param menuElement id or class of element to grab
*/
$scope.showInlinePopup = (ev: any, menuElement: string): void => {
if (!$scope.filterMenu) {
const position: material.IPanelPosition | any = $mdPanel.newPanelPosition()
.relativeTo(ev.srcElement)
.addPanelPosition($mdPanel.xPosition.CENTER, $mdPanel.yPosition.BELOW)
const animation = $mdPanel.newPanelAnimation()
animation.openFrom(position)
animation.closeTo(position)
animation.withAnimation($mdPanel.animation.FADE)
const config: material.IPanelConfig & {
contentElement: string,
openFrom: any
} = {
animation,
attachTo: element(document.body),
contentElement: menuElement,
position,
openFrom: ev,
clickOutsideToClose: true,
escapeToClose: true,
focusOnOpen: false,
zIndex: 2
}
$scope.filterMenu = $mdPanel.create(config)
$scope.filterMenu.reposition = function reposition() {
$timeout(() => {
$scope.filterMenu.updatePosition(position)
}, 100)
}
}
$scope.filterMenu.open()
}
/**
* @param reset - set true to force reset
*/
$scope.getMLSVariables = (reset?: boolean): MLSService[] => {
if (!mlsVariables || reset) {
// mlsVariables = Idx.getMLSVariables()
mlsVariables = {}
Idx.getMLSVariables().forEach((service: MLSService) => {
mlsVariables[service.id] = service
})
}
return Object.values(mlsVariables)
}
/**
* Update the entirety options.query in a safe manner to ensure undefined references are not produced
*/
$scope.setQuery = (newQuery?: CompileFilterOptions): void => {
newQuery ??= {}
newQuery.where ??= {}
// getDefaultWhereOptions returns the set a required WhereOptions with initialized arrays
// $scope.options.query = extend(Idx.getDefaultWhereOptions(), newQuery)
$scope.options.query = cloneDeep(newQuery)
$scope.setWhere($scope.options.query.where)
// console.log('setQuery $scope.options.query to ', clone($scope.options.query))
}
/**
* Update the entirety options.query.where in a safe manner to ensure undefined references are not produced
*/
$scope.setWhere = (newWhere?: WhereOptions): void => {
// console.log('setWhere', clone(newWhere))
newWhere ??= {}
// getDefaultWhereOptions returns the set a required WhereOptions with initialized arrays
$scope.options.query.where = extend(Idx.getDefaultWhereOptions(), newWhere)
// find the objects that aren't arrays and convert to arrays as require to prevent future and current errors
map(Idx.getDefaultWhereOptions(), (value, key: string) => {
if (
isArray(value) &&
Object.prototype.hasOwnProperty.call($scope.options.query.where, key) &&
!isArray($scope.options.query.where[key])
) {
$scope.options.query.where[key] = [$scope.options.query.where[key]]
}
})
$scope.parsePresetLocationText()
// console.log('setWhere', clone($scope.options.query.where))
}
$scope.setWhereDefaults = (): void => {
$scope.$applyAsync(() => {
if ($scope.options.query.where.ListingType.length < 1) {
$scope.options.query.where.ListingType = $scope.options.selection.ListingType.default.Sale.Residential
// console.log('updating', $scope.options.query.where.ListingType)
$scope.selectDefaultListingType()
}
// console.log('setting lastQuery setWhereDefaults', cloneDeep($scope.options.query))
lastQuery = cloneDeep($scope.options.query)
$scope.parsePresetLocationText()
})
}
$scope.selectDefaultListingType = (listingGroup?: string): void => {
if (!listingGroup) {
listingGroup = 'Commercial'
if (!$scope.options.selection.ListingType.group.Commercial) {
listingGroup = 'Residential'
}
}
$scope.options.query.where.ListingType = $scope.options.forRent ?
$scope.options.selection.ListingType.default.Lease[listingGroup] :
$scope.options.selection.ListingType.default.Sale[listingGroup]
if ($scope.filterMenu) {
$scope.filterMenu.reposition()
}
if ($scope.options.forRent) {
$scope.options.query.where.Status = $scope.options.selection.Status.default.Lease
} else {
$scope.options.query.where.Status = ($scope.options.query.where.Status && $scope.options.query.where.Status.length > 0) ?
$scope.options.query.where.Status : $scope.options.selection.Status.default.Lease
}
}
/**
* Call a List widget to perform a search
* TODO await until search is complete?
*/
$scope.search = (force?: boolean): void => {
if (!$scope.initialized) {
console.warn($scope.uid, 'has not initialized and may not search yet')
return
}
let listScope: IdxPropertyListScope | IdxComponentScope
if ($scope.listId) {
listScope = Idx.getListInstance($scope.listId)
}
if (listScope) {
// $scope.options.query.service = [1]
// $scope.options.query.where.Page = 1 // just a fallback, as it gets 'Page 2'
// $scope.options.query.page = 1 // just a fallback, as it gets 'Page 2'
// console.log('sending search', clone($scope.options.query))
/* const searchQuery: CompileFilterOptions = {
where: clone($scope.options.query.where)
}*/
// FIXME need to ensure only where options
// console.log('but suppose to send', clone($scope.options.query))
// listScope.search($scope.options.query, true)
// only allow a query every second
if (!$scope.throttledSearch) {
$scope.throttledSearch =
throttle(() => {listScope.search($scope.options.query, true)}, 600, { trailing: false })
}
$scope.throttledSearch()
} else {
// console.log('comparing last', cloneDeep(lastQuery))
// console.log('comparing current', cloneDeep($scope.options.query))
if ($scope.hasQueryChanged() || force) {
lastQuery = cloneDeep($scope.options.query)
// console.warn('there was a change')
Idx.setUrlOptions('Search', $scope.options.query.where)
// Removing / from #!/, because the UrlOptionsPaths will either be blank or always provide / in front now
$window.open($scope.listLinkUrl + '#!' + Idx.getUrlOptionsPath(defaultQuery), $scope.listLinkTarget)
}
}
}
/**
* Either popup or load a new page with the
*/
$scope.displayOfficeGroupSelector = (searchTerm?: string, editIndex?: number, ev?: any): void => {
if (ev) {
ev.preventDefault()
// ev.stopPropagation()
}
// console.log('displayOfficeGroupSelector', searchTerm, editIndex)
let searchOnLoad = false
const options: {
query: CompileFilterOptions
} = {
query: {
perPage: 100
}
}
if (!isEmpty(searchTerm) && isString(searchTerm)) {
options.query.where = {
OfficeName: searchTerm
}
searchOnLoad = true
}
if (!isNumber(editIndex)) {
editIndex = $scope.options.officeGroups.length
}
const template =
'<md-dialog aria-label="Property Office Group Selector" class="transparent">' +
'<md-button style="text-align: center" data-ng-click="close()">Close and Accept</md-button>' +
'<stratus-idx-office-search' +
' data-template="search.group-selector"' +
` data-list-id="office-group-selector-${$scope.elementId}"` +
` data-options='${JSON.stringify(options)}'` +
` data-sync-instance="${$scope.elementId}"` + // search needs to update this scope
` data-sync-instance-variable="options.officeGroups"` + // search needs to find this variable in this scope to update
` data-sync-instance-variable-index="${editIndex}"` +
'></stratus-idx-office-search>' +
'<stratus-idx-office-list' +
` data-element-id="office-group-selector-${$scope.elementId}"` +
' data-template="list.empty"' +
` data-search-on-load="${searchOnLoad}"` +
` data-query='${JSON.stringify(options.query)}'` +
` data-query-service="${$scope.options.query.service}"` +
'></stratus-idx-office-list>' +
'</md-dialog>'
$mdDialog.show({
template,
parent: element(document.body),
targetEvent: ev,
clickOutsideToClose: true,
fullscreen: true, // Only for -xs, -sm breakpoints.
// bindToController: true,
controllerAs: 'ctrl',
// tslint:disable-next-line:no-shadowed-variable
controller: ($scope: any, $mdDialog: material.IDialogService) => { // shadowing is needed for inline controllers
const dc = this
dc.$onInit = () => {
dc.close = close
}
function close() {
// console.log('closing mdPanel')
if ($mdDialog) {
$mdDialog.hide()
}
}
$scope.close = close
}
})
.then(() => {
$scope.validateOfficeGroups()
}, () => {
$scope.validateOfficeGroups()
// IDX.setUrlOptions('Listing', {})
// IDX.refreshUrlOptions(defaultOptions)
// Revert page title back to what it was
// IDX.setPageTitle()
// Let's destroy it to save memory
// $timeout(IDX.unregisterDetailsInstance('property_member_detail_popup'), 10)
})
}
$scope.validateOfficeGroups = (search?: boolean): void => {
$scope.options.officeGroups = $scope.options.officeGroups.filter((selection) => {
return (!isEmpty(selection.name) && !isEmpty(selection.group))
})
const officeNumbers: string[] = []
$scope.options.query.where.OfficeNumber = [] as string[]
$scope.options.officeGroups.forEach((selection) => {
officeNumbers.push(...selection.group)
})
$scope.options.query.where.OfficeNumber = officeNumbers
if (search) {
$scope.search()
}
}
/**
* Have the widget options refreshed form the Widget's end
*/
$scope.refreshSearchWidgetOptions = async (listScope?: IdxPropertyListScope): Promise<void> => {
if (
!listScope &&
$scope.listId
) {
listScope = Idx.getListInstance($scope.listId) as IdxPropertyListScope
}
if (listScope) {
$scope.setQuery(listScope.query)
lastQuery = cloneDeep($scope.options.query)
$scope.listInitialized = true
}
}
$scope.on = (emitterName: string, callback: IdxEmitter) => Idx.on($scope.elementId, emitterName, callback)
$scope.hasQueryChanged = (): boolean => !isEqual(clone(lastQuery), clone($scope.options.query))
/**
* Destroy this widget
*/
$scope.remove = (): void => {
// TODO need to kill any attached slideshows
}
},
templateUrl: ($attrs: IAttributes): string => `${localDir}${$attrs.template || componentName}.component${min}.html`
}