module Shockout { interface ICurrencyFormat { pos : string ; neg : string ; zero : string ; } export class Utils { param array returns public static tail ( array ) { if ( array == null || array == undefined ) { return array ; } else if ( array . constructor !== Array ) { return array ; } const len = array . length ; if ( len > 0 ) { return array [ len - 1 ] ; } return undefined ; } param a param value return public static indexOf ( a , value ) { if ( ! ! Array . prototype . indexOf ) { return Array . prototype . indexOf . apply ( a , [ value ] ) ; } for ( var i = 0 ; i < a . length ; i ++ ) { if ( a [ i ] === value ) { return i ; } } return - 1 ; } param url return public static formatSubsiteUrl ( url ) { return ! ! ! url ? '/' : ! /\/$/ . test ( url ) ? url + '/' : url ; } param str return public static toCamelCase ( str ) { return str . toString ( ) . replace ( /\s*\b\w/g , function ( x ) { return ( x [ 1 ] || x [ 0 ] ) . toUpperCase ( ) ; } ) . replace ( /\s/g , '' ) . replace ( /\'s/ , 'S' ) . replace ( /[^A-Za-z0-9\s_]/g , '' ) ; } return public static getIdFromHash ( ) { var url = ( window . location != window . parent . location ) ? document . referrer : document . location . href ; var rxHash = /#\/id\/\d+/i ; var exec = rxHash . exec ( url ) ; var id = ! ! exec ? exec [ 0 ] . replace ( /\D/g , '' ) : null ; return /\d/ . test ( id ) ? parseInt ( id ) : null ; } return public static setIdHash ( id ) { window . location . hash = '#/id/' + id ; } /**
        * Escape column values
        * http://dracoblue.net/dev/encodedecode-special-xml-characters-in-javascript/155/
        */ public static escapeColumnValue ( s ) { if ( typeof s === "string" ) { return s . replace ( /&(?![a-zA-Z]{1,8};)/g , "&amp;" ) ; } else { return s ; } } public static getParent ( o , num = 1 ) { for ( var i = 0 ; i < num ; i ++ ) { if ( ! ! ! o ) { continue ; } o = o . parentNode ; } return o ; } public static getPrevKOComment ( o ) { do { o = o . previousSibling ; } while ( o && o . nodeType != 8 && ! /^\s*ko/ . test ( o . textContent ) ) ; return o ; } public static getKoComments ( parent ) { var koNames = [ ] ; parent = parent || $ ( 'body' ) ; $ ( parent ) . contents ( ) . filter ( function ( i , e ) { return e . nodeType == 8 && /^\s*ko/ . test ( e . nodeValue ) ; } ) . each ( function ( i , e ) { koNames . push ( e . nodeValue . replace ( /\s*ko\s*foreach\s*:\s*(\$root\.|)/ , '' ) . replace ( /\s/g , '' ) ) ; } ) ; return koNames ; } public static getKoContainerlessControls ( parent ) { var a = [ ] ; parent = parent || document . body ; $ ( parent ) . contents ( ) . filter ( function ( i , e ) { return e . nodeType == 8 && /^\s*ko\s*foreach:/ . test ( e . nodeValue ) ; } ) . each ( function ( i , e ) { a . push ( e ) ; } ) ; return a ; } public static getEditableKoContainerlessControls ( parent ) { parent = parent || document . body ; var comments = Utils . getKoContainerlessControls ( parent ) ; var a = [ ] ; var rxNotTypes = /(^button|submit|cancel|reset)/i ; var rxTagNames = /(input|textarea)/i ; var rxIsContext = /\$data/ ; for ( var i = 0 ; i < comments . length ; i ++ ) { var next = Utils . getNextSibling ( comments [ i ] ) ; var db = next . getAttribute ( 'data-bind' ) ; if ( ! ! db && rxTagNames . test ( next . tagName ) && rxIsContext . test ( db ) && rxNotTypes . test ( next . getAttribute ( 'type' ) ) ) { a . push ( comments [ i ] ) ; continue ; } var bindings = next . querySelectorAll ( "input[data-bind*='$data']:enabled, textarea[data-bind*='$data']:enabled" ) ; if ( bindings . length > 0 ) { a . push ( comments [ i ] ) ; } } return a ; } public static getEditableKoControlNames ( parent ) { var a = [ ] ; var rxNotTypes = /(button|submit|cancel|reset)/ ; var rx = /\s*:\s*(\$root.|)\w*\b/ ; var replace = $ ( parent ) . find ( '[data-bind]' ) . filter ( ':input' ) . filter ( function ( i , e ) { return ! rxNotTypes . test ( $ ( e ) . attr ( 'type' ) ) ; } ) . each ( clean ) ; $ ( parent ) . find ( '[data-bind][contenteditable="true"]' ) . each ( clean ) ; function clean ( i , e ) { var exec = rx . exec ( $ ( e ) . attr ( 'data-bind' ) ) ; var koName = ! ! exec ? exec [ 0 ] . replace ( /:(\s+|)/g , '' ) . replace ( /\$root\./ , '' ) . replace ( /\s/g , '' ) : null ; if ( koName != null ) { a . push ( koName ) ; } } return a ; } parem return public static getEditableKoNames ( parent ) { parent = parent || document . body ; var a = [ ] ; var rxExcludeInputTypes = /(button|submit|cancel|reset)/ ; $ ( parent ) . find ( '.so-editable[ko-name]' ) . each ( function ( i , el ) { var n = $ ( el ) . attr ( 'ko-name' ) ; if ( Utils . indexOf ( a , n ) < 0 ) { a . push ( n ) ; } } ) ; var comments = Utils . getEditableKoContainerlessControls ( parent ) ; for ( var i = 0 ; i < comments . length ; i ++ ) { var n = comments [ i ] . nodeValue . replace ( /\s*ko\s*foreach\s*:\s*(\$root\.|)/ , '' ) . replace ( /\s/g , '' ) ; if ( Utils . indexOf ( a , n ) < 0 ) { a . push ( n ) ; } } var koNames = Utils . getEditableKoControlNames ( parent ) ; for ( var i = 0 ; i < koNames . length ; i ++ ) { var n = koNames [ i ] ; if ( Utils . indexOf ( a , n ) < 0 ) { a . push ( n ) ; } } return a ; } public static getNextSibling ( el ) { do { el = el . nextSibling ; } while ( el . nodeType != 1 ) ; return el ; } param control return public static observableNameFromControl ( control , vm = undefined ) { var db = control . getAttribute ( 'data-bind' ) ; if ( ! ! ! db ) { return null ; } var koName = $ ( control ) . attr ( 'ko-name' ) ; if ( ! ! koName ) { return koName ; } var rx = /(\b:\s*|\$root\.)\w*\b/ ; var exec = rx . exec ( db ) ; koName = ! ! exec ? exec [ 0 ] . replace ( /:(\s+|)/g , '' ) . replace ( /\$root\./ , '' ) . replace ( /\s/g , '' ) : null ; return koName ; } /**
        * Alias for observableNameFromControl()
        */ public static koNameFromControl = Utils . observableNameFromControl ; public static parseJsonDate ( d ) { if ( ! Utils . isJsonDateTicks ( d ) ) { return null ; } return new Date ( parseInt ( d . replace ( /\D/g , '' ) ) ) ; } public static parseIsoDate ( d ) { if ( ! Utils . isIsoDateString ( d ) ) { return null ; } return new Date ( d ) ; } public static isJsonDateTicks ( val ) { if ( ! ! ! val ) { return false ; } return /\/Date\(\d+\)\// . test ( val + '' ) ; } public static isIsoDateString ( val ) { if ( ! ! ! val ) { return false ; } return /\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z/ . test ( val + '' ) ; } public static getQueryParam ( p ) { var search = '' ; var url = ( window . location != window . parent . location ) ? document . referrer : document . location . href ; if ( url . indexOf ( '?' ) > - 1 ) { search = '?' + url . split ( '?' ) [ 1 ] ; } var escape = window [ "escape" ] , unescape = window [ "unescape" ] ; p = escape ( unescape ( p ) ) ; var regex = new RegExp ( "[?&]" + p + "(?:=([^&]*))?" , "i" ) ; var match = regex . exec ( search ) ; return match != null ? match [ 1 ] : null ; } public static clone ( objectToBeCloned ) { if ( ! ( objectToBeCloned instanceof Object ) ) { return objectToBeCloned ; } var objectClone ; var Constructor = objectToBeCloned . constructor ; switch ( Constructor ) { case RegExp : objectClone = new Constructor ( objectToBeCloned ) ; break ; case Date : objectClone = new Constructor ( objectToBeCloned . getTime ( ) ) ; break ; default : objectClone = new Constructor ( ) ; } for ( var prop in objectToBeCloned ) { objectClone [ prop ] = Utils . clone ( objectToBeCloned [ prop ] ) ; } return objectClone ; } public static logError ( msg , errorLogListName , siteUrl = '/' , debug = false ) { if ( debug || ! SPForm . enableErrorLog ) { throw msg ; } siteUrl = Utils . formatSubsiteUrl ( siteUrl ) ; var loc = window . location . href ; var errorMsg = '<p>An error occurred at <a href="' + loc + '" target="_blank">' + loc + '</a></p><p>Message: ' + msg + '</p>' ; var $jqXhr = $ . ajax ( { url : siteUrl + "_vti_bin/listdata.svc/" + errorLogListName . replace ( /\s/g , '' ) , type : "POST" , processData : false , contentType : "application/json;odata=verbose" , data : JSON . stringify ( { "Title" : "Web Form Error" , "Error" : errorMsg } ) , headers : { "Accept" : "application/json;odata=verbose" } } ) ; $jqXhr . fail ( function ( data ) { throw data . responseJSON . error ; } ) ; } public static updateKoField ( el , val ) { if ( el . tagName . toLowerCase ( ) == "input" ) { $ ( el ) . val ( val ) ; } else { $ ( el ) . html ( val ) ; } } public static validateSpPerson ( person ) { return person != null && person . toString ( ) . match ( /^\d*;#/ ) != null ; } public static isTime ( val ) { if ( ! ! ! val ) { return false ; } var rx = /\d{1,2}:\d{2}(:\d{2}|)\s{0,1}(AM|PM)/ ; return rx . test ( val ) ; } public static isDate ( val ) { if ( ! ! ! val ) { return false ; } var rx = /\d{1,2}\/\d{1,2}\/\d{4}/ ; return rx . test ( val . toString ( ) ) ; } public static dateToLocaleString ( d ) { try { var dd = d . getUTCDate ( ) ; dd = dd < 10 ? "0" + dd : dd ; var mo = d . getUTCMonth ( ) + 1 ; mo = mo < 10 ? "0" + mo : mo ; return mo + '/' + dd + '/' + d . getUTCFullYear ( ) ; } catch ( e ) { return 'Invalid Date' ; } } public static toTimeLocaleObject ( d ) { var hours = 0 ; var minutes ; var tt ; hours = d . getUTCHours ( ) ; minutes = d . getUTCMinutes ( ) ; tt = hours > 11 ? 'PM' : 'AM' ; if ( minutes < 10 ) { minutes = '0' + minutes ; } if ( hours > 12 ) { hours -= 12 ; } return { hours : hours , minutes : minutes , tt : tt } ; } public static toTimeLocaleString ( d ) { var str = '12:00 AM' ; var hours = d . getUTCHours ( ) ; var minutes = d . getUTCMinutes ( ) ; var tt = hours > 11 ? 'PM' : 'AM' ; if ( minutes < 10 ) { minutes = '0' + minutes ; } if ( hours > 12 ) { hours -= 12 ; } else if ( hours == 0 ) { hours = 12 ; } return hours + ':' + minutes + ' ' + tt ; } public static toDateTimeLocaleString ( d ) { var time = Utils . toTimeLocaleString ( d ) ; return Utils . dateToLocaleString ( d ) + ' ' + time ; } param val return public static parseDate ( val ) { if ( ! ! ! val ) { return null ; } if ( typeof val == 'object' && val . constructor == Date ) { return val ; } var rxSlash = /^\d{1,2}\/\d{1,2}\/\d{2,4}$/ , rxHyphen = /^\d{1,2}-\d{1,2}-\d{2,4}$/ , rxIsoDate = /^\d{4}-\d{1,2}-\d{1,2}$/ , rxTicks = /(\/|)\d{13}(\/|)/ , rxIsoDateTime = /^\d{4}-\d{2}-\d{2}T\d{1,2}:\d{1,2}:\d{1,2}Z$/ , tmp , m , d , y , date = null ; if ( rxIsoDateTime . test ( val ) ) { return new Date ( val ) ; } val = rxIsoDate . test ( val ) ? val : ( val + '' ) . replace ( /[^0-9\/\-]/g , '' ) ; if ( val == '' ) { return null ; } if ( rxSlash . test ( val ) || rxHyphen . test ( val ) ) { tmp = rxSlash . test ( val ) ? val . split ( '/' ) : val . split ( '-' ) ; m = parseInt ( tmp [ 0 ] ) - 1 ; d = parseInt ( tmp [ 1 ] ) ; y = parseInt ( tmp [ 2 ] ) ; y = y < 100 ? 2000 + y : y ; date = new Date ( y , m , d , 0 , 0 , 0 , 0 ) ; } else if ( rxIsoDate . test ( val ) ) { tmp = val . split ( '-' ) ; y = parseInt ( tmp [ 0 ] ) ; m = parseInt ( tmp [ 1 ] ) - 1 ; d = parseInt ( tmp [ 2 ] ) ; y = y < 100 ? 2000 + y : y ; date = new Date ( y , m , d , 0 , 0 , 0 , 0 ) ; } else if ( rxTicks . test ( val ) ) { date = new Date ( parseInt ( val . replace ( /\D/g , '' ) ) ) ; } return date ; } /**
        * Addapted from accounting.js library. http://josscrowcroft.github.com/accounting.js/
        * Format a number into currency
        *
        * Usage: accounting.formatMoney(number, symbol, precision, thousandsSep, decimalSep, format)
        * defaults: (0, "$", 2, ",", ".", "%s%v")
        *
        * Localise by overriding the symbol, precision, thousand / decimal separators and format
        * Second param can be an object matching `settings.currency` which is the easiest way.
        *
        * To do: tidy up the parameters
        */ public static formatMoney ( value , symbol = '$' , precision = 2 ) { var num = Utils . unformatNumber ( value ) , format = '%s%v' , neg = format . replace ( '%v' , '-%v' ) , useFormat = num > 0 ? format : num < 0 ? neg : format , numFormat = Utils . formatNumber ( Math . abs ( num ) , Utils . checkPrecision ( precision ) ) ; return useFormat . replace ( '%s' , symbol ) . replace ( '%v' , numFormat ) ; } /**
        * Addapted from accounting.js library. http://josscrowcroft.github.com/accounting.js/
        * Takes a string/array of strings, removes all formatting/cruft and returns the raw float value
        * alias: accounting.`parse(string)`
        *
        * Decimal must be included in the regular expression to match floats (defaults to
        * accounting.settings.number.decimal), so if the number uses a non-standard decimal
        * separator, provide it as the second argument.
        *
        * Also matches bracketed negatives (eg. "$ (1.99)" => -1.99)
        *
        * Doesn't throw any errors (`NaN`s become 0) but this may change in future
        */ public static unformatNumber ( value ) { if ( typeof value === "number" ) return value ; var unformatted = parseFloat ( ( value + '' ) . replace ( /\((.*)\)/ , '-$1' ) . replace ( /[^0-9-.]/g , '' ) ) ; return ! isNaN ( unformatted ) ? unformatted : 0 ; } /**
        * Addapted from accounting.js library. http://josscrowcroft.github.com/accounting.js/
        * Format a number, with comma-separated thousands and custom precision/decimal places
        *
        * Localise by overriding the precision and thousand / decimal separators
        * 2nd parameter `precision` can be an object matching `settings.number`
        */ public static formatNumber ( value , precision = 0 ) { var num = Utils . unformatNumber ( value ) , usePrecision = Utils . checkPrecision ( precision ) , negative = num < 0 ? "-" : "" , base = parseInt ( Utils . toFixed ( Math . abs ( num || 0 ) , usePrecision ) , 10 ) + "" , mod = base . length > 3 ? base . length % 3 : 0 ; return negative + ( mod ? base . substr ( 0 , mod ) + ',' : '' ) + base . substr ( mod ) . replace ( /(\d{3})(?=\d)/g , '$1,' ) + ( usePrecision ? '.' + Utils . toFixed ( Math . abs ( num ) , usePrecision ) . split ( '.' ) [ 1 ] : "" ) ; } /**
         * Tests whether supplied parameter is a string
         * from underscore.js
         */ public static isString ( obj ) { return ! ! ( obj === '' || ( obj && obj . charCodeAt && obj . substr ) ) ; } /**
        * Addapted from accounting.js library.
        * Implementation of toFixed() that treats floats more like decimals
        *
        * Fixes binary rounding issues (eg. (0.615).toFixed(2) === "0.61") that present
        * problems for accounting- and finance-related software.
        */ public static toFixed ( value , precision = 0 ) { precision = Utils . checkPrecision ( precision ) ; var power = Math . pow ( 10 , precision ) ; return ( Math . round ( Utils . unformatNumber ( value ) * power ) / power ) . toFixed ( precision ) ; } /**
        * Addapted from accounting.js library. http://josscrowcroft.github.com/accounting.js/
        * Check and normalise the value of precision (must be positive integer)
        */ public static checkPrecision ( val ) { val = Math . round ( Math . abs ( val ) ) ; return isNaN ( val ) ? 0 : val ; } param array1 param array2 return public static compareArrays ( array1 , array2 ) { var results = [ ] ; for ( var i = 0 ; i < array1 . length ; i ++ ) { for ( var j = 0 ; j < array2 . length ; j ++ ) { if ( array1 [ i ] == array2 [ j ] && Utils . indexOf ( results , array2 [ j ] ) < 0 ) { results . push ( array2 [ j ] ) ; } } } return results ; } public static trim ( str ) { if ( ! Utils . isString ( str ) ) { return str ; } return str . replace ( /(^\s+|\s+$)/g , '' ) ; } public static formatPictureUrl ( pictureUrl ) { return pictureUrl == null ? '/_layouts/images/person.gif' : pictureUrl . indexOf ( ',' ) > - 1 ? pictureUrl . split ( ',' ) [ 0 ] : pictureUrl ; } public static isZrow ( node ) { return node . nodeName . toLowerCase ( ) == 'z:row' ; } } }