ldx-widgets
Version:
widgets
355 lines (287 loc) • 11.7 kB
text/coffeescript
React = require 'react'
moment = require 'moment'
_ = require 'lodash'
{div, a} = React.DOM
module.exports =
###
* Return Age from datestring
* @param {String} datestring
* @param {Boolean} return the month default false.
###
formatAge: (datestring, includeMonth) ->
now = moment().utc()
birthDate = moment(datestring).utc()
age = ''
months = now.diff(birthDate, 'months')
if months < 1
days = now.diff(birthDate, 'days')
age = "#{days} DO"
else if months < 48
age = "#{months} MO"
else
years = now.diff(birthDate, 'years')
age = "#{years} YO"
# Include month in age
if includeMonth
months = months-(years*12)
# don't include if 0 months
if months then age = "#{years} yrs #{months} mnth"
return age
formatNHS: (nhs = '') ->
# Strip non digit characters out of the nhs
nhs = nhs.replace(/[^0-9]/g, "")
nhs1 = nhs.substr(0,3)
nhs2 = nhs.substr(3,3)
nhs3 = nhs.substr(6,4)
rv = "#{nhs1}"
rv += " #{nhs2}" if nhs2
rv += " #{nhs3}" if nhs3
rv
# Returns 10 digit number in (xxx) xxx-xxxx format
# or 7 digit number in xxx-xxxx format
formatPhoneNumber: (phone) ->
if phone?
# Make sure the number is a String
phone = "#{phone}"
phoneClone = phone
phoneClone = phoneClone.replace(/[^0-9]/g, '')
if phoneClone.length == 10
phoneClone = phoneClone.replace(/(\d{3})(\d{3})(\d{4})/, "($1) $2-$3")
return phoneClone
if phoneClone.length == 7
phoneClone = phoneClone.replace(/(\d{3})(\d{4})/, "$1-$2")
return phoneClone
else
return phone
else
return ""
formatSource: (options) ->
{orgName, facilityName, systemName} = options
sourceName = ''
# Org
if orgName?
sourceName += orgName
# Facility
if facilityName? and facilityName not in [orgName, '']
sourceName += ' - ' if sourceName isnt ''
sourceName += facilityName
# System
if (facilityName? or orgName?) and systemName? and systemName isnt ''
sourceName += ' - ' if sourceName isnt ''
sourceName += systemName
sourceName
formatTimezoneForAPI: (relevantDate) ->
minutes = moment(relevantDate).utcOffset()
if minutes < 0 then negative = "-"
else negative = "+"
minutes = Math.abs(minutes)
hours = Math.floor(minutes / 60)
if hours < 10 then hours = "0" + hours
minutestring = minutes % 60
if minutestring < 10 then minutestring = "0" + minutestring
"#{negative}#{hours}:#{minutestring}"
escapeRegExp: (string) ->
string.replace(/([.*+?^=!:${}()|\[\]\/\\])/g, "\\$1")
isValidEmail : (addr) ->
# modified RFC 2822 - http://www.regular-expressions.info/email.html
return /[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*@(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?/.test addr
# Adding comment to jenkins/github integration.
# Converts raw bytes to KB, MB, etc
bytesToSize: (bytes, precision) ->
unless bytes? then return ''
kilobyte = 1024
megabyte = kilobyte * 1024
gigabyte = megabyte * 1024
terabyte = gigabyte * 1024
if bytes >= 0 and bytes < kilobyte then return bytes + ' B'
else if bytes >= kilobyte and bytes < megabyte then return (bytes / kilobyte).toFixed(precision) + ' KB'
else if bytes >= megabyte and bytes < gigabyte then return (bytes / megabyte).toFixed(precision) + ' MB'
else if bytes >= gigabyte and bytes < terabyte then return (bytes / gigabyte).toFixed(precision) + ' GB'
else if bytes >= terabyte then return (bytes / terabyte).toFixed(precision) + ' TB'
else return bytes + ' B'
measureScrollBarWidth: ->
$tester = $("<div id='outer' style='overflow: scroll; height: 500px; width: 500px; position: absolute; top: 100px; left: 100px;'><div id='inner' style='position: absolute; height: 100%; width: 100%;'></div><div style='height: 600px; width: 600px;'></div></div>")
$('body').append $tester
scrollBarWidth = $tester.height() - $tester.find('#inner').height()
$tester.remove()
return scrollBarWidth
# Reduces name to targetLength by replacing a middle portion of the name with an ellipses
formatFileName: (name, targetLength) ->
# Get the starting file name length
fileLength = name.length
# Do nothing to files with less than targetLength characters
if fileLength <= targetLength then return name
# Calculate the number of characters that need to be removed to get the filename down to targetLength
# Add 3 to file length as the file length will technically get larger with the ellipses thus we need to increase the file name length
# NOTE : it was adding 3 to the targetLength but doing that caused for overlapping characters in the file name
removeCount = (fileLength + 3) - targetLength
# Figure out how far from the end we should keeps characters
# In most cases this will be 6 characters unless the diff between the target length and the removeCount is 6 or else
# This will insure the beginning of the file has more than 1 or 2 characters before the ellipses (unless the target length is extremely small like 3)
if (targetLength - removeCount) <= 6 then charactersFromEnd = 4 else charactersFromEnd = 6
# Calculate the place the remove should end
endRemove = fileLength - charactersFromEnd
# Calculate the postion to start the remove
startRemove = endRemove - removeCount
# NOTE: Negative start values cannot be used in substr becuase IE8 does not support them!
# Get the start of the file
fileStart = name.substr(0, startRemove)
# Get the end of the file
fileEnd = name.substr(endRemove)
return fileStart + "…" + fileEnd
# Gets file extensions from a file name, returns empty string when there is no extensions
parseFileExtension: (filename) ->
filenameSplit = filename.split('.')
return name = if filenameSplit.length > 1 then filenameSplit[filenameSplit.length - 1] else ''
# Makes a temparay (somewhat unique) id for use in the GUI before saving a new object to the server
makeGuid: ->
'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace /[xy]/g, (c) ->
r = Math.random()*16|0
v = if c is 'x' then r else (r&0x3|0x8)
v.toString(16)
# Checks if two object have the same id or guid
idsMatch: (itemA, itemB) ->
unless itemA? and itemB? then return no
if itemA.id? and itemB.id?
return itemA.id is itemB.id
else if itemA.guid? and itemB.guid?
return itemA.guid is itemB.guid
else
return no
# Create a fake mouse event
synthesizeMouseEvent: (target, type, options = {}) ->
event = target.ownerDocument.createEvent('MouseEvents')
opts =
type: type
canBubble: false # Defaults to false: may need to set to true if we run into a situation where React has issues with events not bubbling to the top
cancelable: true
view: target.ownerDocument.defaultView
detail: 1
screenX: 0 # The coordinates within the entire page
screenY: 0
clientX: 0 # The coordinates within the viewport
clientY: 0
ctrlKey: false
altKey: false
shiftKey: false
metaKey: false
button: 0 # 0 = left, 1 = middle, 2 = right
relatedTarget: null
# Merge the options with the defaults
_.assign(opts, options)
# Pass in the options
event.initMouseEvent(
opts.type
opts.canBubble
opts.cancelable
opts.view
opts.detail
opts.screenX
opts.screenY
opts.clientX
opts.clientY
opts.ctrlKey
opts.altKey
opts.shiftKey
opts.metaKey
opts.button
opts.relatedTarget
)
# Fire the event
target.dispatchEvent(event)
# Checks to see if input is a number
isNumber: (input) ->
return !isNaN(parseFloat(input)) and isFinite(input)
# Limits a value to only two decimal places
toFixed: (value) ->
return Math.round(value * 100) / 100
# Can measure widths and heights of DOM elements
measureDOMProp: (el, DOMProp) ->
measurer = document.getElementById 'measurer'
measurer.appendChild el
prop = el[DOMProp]
measurer.removeChild el
prop
# Make sure URL begins with 'https'
checkHTTPS: (url) ->
checkString = url.slice 0, 5
return checkString is "https"
buildFormData: (context = @, newData = {}) ->
refs = context['refs']
unless refs? then throw Error("Form Data Builder: No 'refs' object found on context #{context}")
####################
# Define the iterate function that will run for each key part
####################
iterate = (data, keyParts, getValue) =>
# Nested properties
cur = keyParts[0]
next = data
matchArray = cur.match(/\[(\d+)\]/)
matchCollection = cur.match(/\[\{(.+)\}\]/)
nextIsArray = keyParts[1].match(/\[(.+)\]/) if keyParts[1]?
arrayKey = matchArray[1] if matchArray?
collectionKey = matchCollection[1].split(':') if matchCollection?
# Check the object to establish the default type
# Arrays
if arrayKey
unless data? then data = []
next = data[arrayKey]
cur = arrayKey
# Collections
else if collectionKey
unless data? then data = []
collectionRecord = null
k = collectionKey[0]
v = collectionKey[1]
# Check if the value contains a state variable
valRef = v.match(/\$(\w+)/)
if valRef?
val = context['state']
# Make sure state is defined
unless val? then throw Error("Form Data Builder: unable to pull state properties when local state is undefined")
# Double-nested refs
subKeys = v.replace(/^\$/, '').split('.')
while subKeys.length
val = val[subKeys.shift()]
v = val
for obj, i in data when obj?
# Cast the prop value to a string, in case of a number/boolean type
if obj[k]? and String(obj[k]) is String(v)
collectionRecord = next = data[i]
break
unless collectionRecord?
# If no record is found, push a new record in with the matching property
d = {}
d[k] = v
data.push(d)
next = data[data.length - 1]
# Object
else if not data[cur]?
next = data[cur] = if nextIsArray then [] else {}
# If it already exists
else
next = data[cur]
# Base case. Return the value of the field once the destination prop has been reached
unless keyParts.length > 1
data[cur] = getValue()
return data
# Remove the first item to continue recursion
keyParts.shift()
return iterate(next, keyParts, getValue)
####################
# Loop through the refs, running iterate for each key part
# Refs can be skipped by prepending with '!'
####################
for key, ref of refs when key[0] isnt '!' and ref.getValue?
# Check for nested properties
if key.match(/\.(?![^[\]]*])/)
keyParts = key.split(/\.(?![^[\]]*])/g)
getValue = ref.getValue
# Don't move on unless getValue exists
unless getValue? then throw Error("Ref #{key} does not have a getValue method defined")
# Iterate over each key
iterate(newData, keyParts, getValue)
# Set properties directly when not using nesting
else
newData[key] = ref.getValue()
return newData