@stratusjs/idx
Version:
AngularJS idx/property Service and Components bundle to be used as an add on to StratusJS
585 lines (532 loc) • 24.4 kB
text/typescript
/**
* @file IdxOfficeList Component @stratusjs/idx/office/list.component
* @example <stratus-idx-office-list>
* @see https://github.com/Sitetheory/stratus/wiki/Idx-Office-List-Widget
*/
// Runtime
import {clone, cloneDeep, forEach, isNil, isNumber, isString} from 'lodash'
import {Stratus} from '@stratusjs/runtime/stratus'
import {
element,
material,
IAnchorScrollService,
IAttributes,
ITimeoutService,
IQService,
IWindowService
} from 'angular'
import 'angular-material'
import 'angular-sanitize'
import {
CompileFilterOptions,
IdxEmitter,
IdxListScope,
IdxService,
Office,
WhereOptions
} from '@stratusjs/idx/idx'
import {Collection} from '@stratusjs/angularjs/services/collection'
import {isJSON, LooseObject, safeUniqueId} from '@stratusjs/core/misc'
import {cookie} from '@stratusjs/core/environment'
// Environment
const min = !cookie('env') ? '.min' : ''
const packageName = 'idx'
const moduleName = 'office'
const componentName = 'list'
// There is not a very consistent way of pathing in Stratus at the moment
const localDir = `${Stratus.BaseUrl}${Stratus.DeploymentPath}@stratusjs/${packageName}/src/${moduleName}/`
export type IdxOfficeListScope = IdxListScope<Office> & {
// options: CompileFilterOptions // FIXME rename to query
initialized: boolean
urlLoad: boolean
searchOnLoad: boolean
query: CompileFilterOptions
detailsLinkPopup: boolean
detailsLinkUrl: string
detailsLinkTarget: 'popup' | '_self' | '_blank'
googleApiKey?: string
variableSyncing?: LooseObject
displayModelDetails(model: Office, ev?: any): void
variableInject(member: Office): Promise<void>
}
Stratus.Components.IdxOfficeList = {
bindings: {
// TODO wiki docs
elementId: '@',
tokenUrl: '@',
urlLoad: '@',
searchOnLoad: '@',
detailsLinkPopup: '@',
detailsLinkUrl: '@',
detailsLinkTarget: '@',
orderOptions: '@',
query: '@',
/** Type: number[] */
queryService: '@',
template: '@',
variableSync: '@'
},
controller(
$anchorScroll: IAnchorScrollService,
$attrs: IAttributes,
$q: IQService,
$mdDialog: material.IDialogService,
$timeout: ITimeoutService,
$scope: IdxOfficeListScope,
$window: IWindowService,
Idx: IdxService,
) {
// Initialize
$scope.uid = safeUniqueId(packageName, moduleName, componentName)
$scope.elementId = $attrs.elementId || $scope.uid
Stratus.Instances[$scope.elementId] = $scope
if ($attrs.tokenUrl) {
Idx.setTokenURL($attrs.tokenUrl)
}
// Stratus.Internals.CssLoader(`${localDir}${$attrs.template || componentName}.component${min}.css`)
let defaultOptions: WhereOptions
let defaultQuery: WhereOptions
let lastQuery: CompileFilterOptions
/**
* All actions that happen first when the component loads
* Needs to be placed in a function, as the functions below need to the initialized first
*/
const init = async () => {
/**
* Allow options to be loaded initially from the URL
*/
$scope.urlLoad = $attrs.urlLoad && isJSON($attrs.urlLoad) ? JSON.parse($attrs.urlLoad) : false
$scope.searchOnLoad = $attrs.searchOnLoad && isJSON($attrs.searchOnLoad) ? JSON.parse($attrs.searchOnLoad) : false
$scope.detailsLinkPopup = $attrs.detailsLinkPopup && isJSON($attrs.detailsLinkPopup) ?
JSON.parse($attrs.detailsLinkPopup) : true
$scope.detailsLinkUrl = $attrs.detailsLinkUrl || '/property/office/details'
$scope.detailsLinkTarget = $attrs.detailsLinkTarget || '_self'
$scope.query = $attrs.query && isJSON($attrs.query) ? JSON.parse($attrs.query) : {}
$scope.query.service = $attrs.queryService && isJSON($attrs.queryService) ?
JSON.parse($attrs.queryService) : $scope.query.service || []
$scope.query.order =
$scope.query.order && isString($scope.query.order) && isJSON($scope.query.order) ? JSON.parse($scope.query.order) :
$attrs.queryOrder && isJSON($attrs.queryOrder) ? JSON.parse($attrs.queryOrder) : $scope.query.order || null
$scope.query.page ??= null// will be set by Service
$scope.query.perPage ??=
($attrs.queryPerPage && isString($attrs.queryPerPage) ? parseInt($attrs.queryPerPage, 10) : null) ||
($attrs.queryPerPage && isNumber($attrs.queryPerPage) ? $attrs.queryPerPage : null) ||
25
$scope.query.where = $attrs.queryWhere && isJSON($attrs.queryWhere) ? JSON.parse($attrs.queryWhere) : $scope.query.where || []
// Fixme, sometimes it's just MemberMlsAccessYN ....
// $scope.query.where.MemberStatus = $scope.query.where.MemberStatus || {inq: ['Active', 'Yes', 'TRUE']}
// $scope.query.where.MemberKey = $scope.query.where.MemberKey || '91045'
// $scope.query.where.AgentLicense = $scope.query.where.AgentLicense || []*/
defaultOptions = JSON.parse(JSON.stringify($scope.query.where))// Extend/clone doesn't work for arrays
defaultQuery = JSON.parse(JSON.stringify($scope.query.where)) // Extend/clone doesn't work for arrays
// lastQuery = {}
/* $scope.orderOptions ??= {
'Price (high to low)': '-ListPrice',
'Price (low to high)': 'ListPrice'
} */
// $scope.googleApiKey = $attrs.googleApiKey || null
// Register this List with the Property service
Idx.registerListInstance($scope.elementId, moduleName, $scope)
// const urlOptions: { Search?: any } = {}
// const urlQuery: { Search?: any } = {}
/* if ($scope.urlLoad) {
// first set the UrlOptions via defaults (cloning so it can't be altered)
Idx.setUrlOptions('Search', JSON.parse(JSON.stringify(defaultQuery)))
// Load Options from the provided URL settings
urlOptions = Idx.getOptionsFromUrl()
// If a specific listing is provided, be sure to pop it up as well
if (
urlOptions.Listing.service
&& urlOptions.Listing.ListingKey
) {
$scope.displayPropertyDetails(urlOptions.Listing)
}
} */
// if ($scope.urlLoad) {
// await $scope.search(urlQuery.Search, true, false)
// }
if ($scope.searchOnLoad) {
await $scope.search($scope.query, false, false)
}
$scope.$applyAsync(() => {
$scope.initialized = true
Idx.emit('init', $scope)
})
}
// Initialization by Event
this.$onInit = () => {
$scope.Idx = Idx
$scope.collection = new Collection<Office>({})
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) => {
if (initNowCtrl !== true) {
return
}
if (!$scope.initialized) {
init().then()
}
stopWatchingInitNow()
})
}
$scope.$watch('collection.models', () => { // models?: []
if ($scope.collection.completed) {
Idx.emit('collectionUpdated', $scope, clone($scope.collection))
}
})
$scope.getPageModels = (): Office[] => {
// console.log('checking $scope.collection.models', $scope.collection.models)
const offices: Office[] = []
// only get the page's models, not every single model in collection
const models = $scope.collection.models as Office[]
models.slice(
($scope.query.perPage * ($scope.query.page - 1)), // 20 * (1 - 1) = 0. 20 * (2 - 1) = 20
($scope.query.perPage * $scope.query.page) // e.g. 20 * 1 = 20. 20 * 2 = 40
).forEach((member) => {
offices.push(member)
})
return offices
}
$scope.scrollToModel = (model: Office): void => {
$anchorScroll(`${$scope.elementId}_${model._id}`)
}
/**
* Functionality called when a search widget runs a query after the page has loaded
* may update the URL options, so it may not be ideal to use on page load
* TODO Idx needs to export search options interface
*/
$scope.search = async (
query?: CompileFilterOptions, // FIXME rename to query
refresh?: boolean,
updateUrl?: boolean
): Promise<Collection<Office>> =>
$q(async (resolve: any) => {
query ??= {}
updateUrl = updateUrl === false ? updateUrl : true
// console.log('searching for', clone(query))
// If refreshing, reset to page 1
/*if (refresh) {
$scope.query.page = 1
}*/
// If search query sent, update the Widget. Otherwise use the widgets current where settings
if (Object.keys(query.where).length > 0) {
// delete ($scope.query.where)
$scope.query.where = query.where
/*if ($scope.query.where.Page) {
$scope.query.page = $scope.query.where.Page
delete ($scope.query.where.Page)
}
if ($scope.query.where.Order) {
$scope.query.order = $scope.query.where.Order
delete ($scope.query.where.Order)
}*/
} /*else {
urlWhere = $scope.query.where || {}
}*/
/* If a different page, set it in the URL
if ($scope.query.page) {
query.Page = $scope.query.page
}
// Don't add Page/1 to the URL
if (query.Page <= 1) {
delete (query.Page)
}*/
// console.log('searching now for', clone(query))
// Page checks
// If a different page, set it in the URL
// If Page was placed in the where query, let's move it
if (query.where.page) {
query.page = query.where.page
delete query.where.page
}
if (query.where.Page) {
query.page = query.where.Page
delete query.where.Page
}
if (query.page) {
$scope.query.page = query.page
}
// If refreshing, reset to page 1
if (refresh) {
$scope.query.page = 1
}
/*if ($scope.query.page) {
urlWhere.Page = $scope.query.page
}
// Don't add Page/1 to the URL
if (urlWhere.Page <= 1) {
delete (urlWhere.Page)
}*/
// If Order was placed in the where query, let's move it
if (query.where.order) {
query.order = query.where.order
delete query.where.order
}
if (query.where.Order) {
query.order = query.where.Order
delete query.where.Order
}
if (query.order) {
$scope.query.order = query.order
// delete ($scope.query.where.Order)
}
/*if ($scope.query.order && $scope.query.order.length > 0) {
query.Order = $scope.query.order
}*/
// Set the URL options
// Idx.setUrlOptions('Search', options)
// Display the URL options in the address bar
/* if (updateUrl) {
Idx.refreshUrlOptions(defaultQuery)
} */
if (
query.hasOwnProperty('service') &&
!isNil(query.service)
) {
// service does not affect URLs as it's a page specific thing
$scope.query.service = query.service
}
Idx.emit('searching', $scope, clone($scope.query))
// Grab the new member listings
// console.log('fetching members:', $scope.query)
try {
// resolve(Idx.fetchProperties($scope, 'collection', $scope.query, refresh))
// Grab the new property listings
const results = await Idx.fetchOffices($scope.elementId, 'collection', $scope.query, refresh)
lastQuery = cloneDeep($scope.query)
Idx.emit('searched', $scope, clone($scope.query))
resolve(results)
} catch (e) {
console.error('Unable to fetchMembers:', e)
}
})
/**
* Move the displayed listings to a different page, keeping the current query
* @param pageNumber - page number
* @param ev - Click event
*/
$scope.pageChange = async (pageNumber: number, ev?: any): Promise<void> => {
if ($scope.collection.pending) {
// Do do anything if the collection isn't ready yet
return
}
// Idx.emit('pageChanging', $scope, clone($scope.query.page))
Idx.emit('pageChanging', $scope, clone($scope.query.page))
if (ev) {
ev.preventDefault()
}
$scope.query.page = pageNumber
await $scope.search()
// Idx.emit('pageChanged', $scope, clone($scope.query.page))
Idx.emit('pageChanged', $scope, clone($scope.query.page))
}
/**
* Move the displayed listings to the next page, keeping the current query
* @param ev - Click event
*/
$scope.pageNext = async (ev?: any): Promise<void> => {
if ($scope.collection.pending) {
// Do do anything if the collection isn't ready yet
return
}
if (!$scope.query.page) {
$scope.query.page = 1
}
if ($scope.collection.completed && $scope.query.page < $scope.collection.meta.data.totalPages) {
if (isString($scope.query.page)) {
$scope.query.page = parseInt($scope.query.page, 10)
}
await $scope.pageChange($scope.query.page + 1, ev)
}
}
/**
* Move the displayed listings to the previous page, keeping the current query
* @param ev - Click event
*/
$scope.pagePrevious = async (ev?: any): Promise<void> => {
if ($scope.collection.pending) {
// Do do anything if the collection isn't ready yet
return
}
if (!$scope.query.page) {
$scope.query.page = 1
}
if ($scope.collection.completed && $scope.query.page > 1) {
if (isString($scope.query.page)) {
$scope.query.page = parseInt($scope.query.page, 10)
}
const prev = $scope.query.page - 1 || 1
await $scope.pageChange(prev, ev)
}
}
/**
* Change the Order/Sorting method and refresh new results
* @param order - string and strings
* @param ev - Click event
*/
$scope.orderChange = async (order: string | string[], ev?: any): Promise<void> => {
if ($scope.collection.pending) {
// Do do anything if the collection isn't ready yet
// TODO set old Order back?
return
}
Idx.emit('orderChanging', $scope, clone(order))
if (ev) {
ev.preventDefault()
}
$scope.query.order = order
await $scope.search(null, true, true)
Idx.emit('orderChanged', $scope, clone(order))
}
$scope.highlightModel = (model: Office, timeout?: number): void => {
timeout ??= 0
model._unmapped ??= {}
$scope.$applyAsync(() => {
model._unmapped._highlight = true
})
if (timeout > 0) {
$timeout(() => {
$scope.unhighlightModel(model)
}, timeout)
}
}
$scope.unhighlightModel = (model: Office): void => {
if (model) {
model._unmapped ??= {}
$scope.$applyAsync(() => {
model._unmapped._highlight = false
})
}
}
/**
* Either popup or load a new page with the
* @param model - details object
* @param ev - Click event
*/
$scope.displayModelDetails = (model: Office, ev?: any): void => {
if (ev) {
ev.preventDefault()
// ev.stopPropagation()
}
if ($scope.detailsLinkPopup === true) {
// Opening a popup will load the propertyDetails and adjust the hashbang URL
const templateOptions: {
element_id: string,
service: number,
'member-key': string,
'page-title': boolean,
'google-api-key'?: string,
} = {
element_id: 'property_member_detail_popup',
service: model._ServiceId,
'member-key': model.MemberKey,
'page-title': true// update the page title
}
if ($scope.googleApiKey) {
templateOptions['google-api-key'] = $scope.googleApiKey
}
let template =
'<md-dialog aria-label="' + model.MemberKey + '">' +
'<stratus-idx-member-details '
forEach(templateOptions, (optionValue, optionKey) => {
template += `${optionKey}='${optionValue}'`
})
template +=
'></stratus-idx-member-details>' +
'</md-dialog>'
$mdDialog.show({
template,
parent: element(document.body),
targetEvent: ev,
clickOutsideToClose: true,
fullscreen: true // Only for -xs, -sm breakpoints.
})
.then(() => {
}, () => {
// Idx.setUrlOptions('Listing', {})
// Idx.refreshUrlOptions(defaultQuery)
// Revery page title back to what it was
Idx.setPageTitle()
// Let's destroy it to save memory
$timeout(() => Idx.unregisterDetailsInstance('property_member_detail_popup', 'member'), 10)
})
} else {
$window.open($scope.getDetailsURL(model), $scope.detailsLinkTarget)
}
}
/**
* Sync Gutensite form variables to a Stratus scope
* TODO move this to it's own directive/service
*/
$scope.variableInject =
async (member: Office): Promise<void> => {
$scope.variableSyncing = $attrs.variableSync && isJSON($attrs.variableSync) ? JSON.parse($attrs.variableSync) : {}
Object.keys($scope.variableSyncing).forEach(elementId => {
// promises.push(
// $q(async function (resolve, reject) {
const varElement = Idx.getInput(elementId)
if (varElement) {
// Form Input exists
if (Object.prototype.hasOwnProperty.call(member, $scope.variableSyncing[elementId])) {
varElement.val(member[$scope.variableSyncing[elementId]])
} else if (
$scope.variableSyncing[elementId] === 'MemberFullName' &&
Object.prototype.hasOwnProperty.call(member, 'MemberFirstName') &&
Object.prototype.hasOwnProperty.call(member, 'MemberLastName')
) {
varElement.val(member.MemberFirstName + ' ' + member.MemberLastName)
} else if (
$scope.variableSyncing[elementId] === 'MemberFirstName' &&
!Object.prototype.hasOwnProperty.call(member, 'MemberFirstName') &&
Object.prototype.hasOwnProperty.call(member, 'MemberFullName')
) {
const nameArray = member.MemberFullName.split(' ')
const firstName = nameArray.shift()
// let lastName = nameArray.join(' ')
varElement.val(firstName)
} else if (
$scope.variableSyncing[elementId] === 'MemberLastName' &&
!Object.prototype.hasOwnProperty.call(member, 'MemberLastName') &&
Object.prototype.hasOwnProperty.call(member, 'MemberFullName')
) {
const nameArray = member.MemberFullName.split(' ')
// let firstName = nameArray.shift()
const lastName = nameArray.join(' ')
varElement.val(lastName)
} else if ($scope.variableSyncing[elementId] === 'OfficeNumber') {
if (Object.prototype.hasOwnProperty.call(member, 'OfficeMlsId')) {
varElement.val(member.OfficeMlsId)
} else if (Object.prototype.hasOwnProperty.call(member, 'OfficeKey')) {
varElement.val(member.OfficeKey)
}
}
// varElement.val(member.MemberFullName)
// let scopeVarPath = $scope.variableSyncing[elementId]
// convert into a real var path and set the intial value from the exiting form value
// await $scope.updateScopeValuePath(scopeVarPath, varElement.val())
// Creating watcher to update the input when the scope changes
// $scope.$watch(
// scopeVarPath,
// function (value) {
// console.log('updating', scopeVarPath, 'value to', value, 'was', varElement.val())
// varElement.val(value)
// },
// true
// )
}
})
// await $q.all(promises)
}
$scope.on = (emitterName: string, callback: IdxEmitter) => Idx.on($scope.elementId, emitterName, callback)
$scope.remove = (): void => {
}
},
templateUrl: ($attrs: IAttributes): string => `${localDir}${$attrs.template || componentName}.component${min}.html`
}