///////////////////////////////// // General utility library // const icons = { image: "$ayiFileImage", pdf: "$ayiFilePdf", word: "$ayiFileWord", powerpoint: "$ayiFilePowerpoint", excel: "$ayiFileExcel", csv: "$ayiFileCsv", audio: "$ayiFileAudio", video: "$ayiFileVidio", archive: "$ayiFileArchive", code: "$ayiFileCode", text: "$ayiFileAlt", file: "$ayiFile" }; const mimeTypes = { "image/gif": icons.image, "image/jpeg": icons.image, "image/png": icons.image, "image/webp": icons.image, "application/pdf": icons.pdf, "application/msword": icons.word, "application/vnd.openxmlformats-officedocument.wordprocessingml.document": icons.word, "application/mspowerpoint": icons.powerpoint, "application/vnd.openxmlformats-officedocument.presentationml.presentation": icons.powerpoint, "application/msexcel": icons.excel, "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet": icons.excel, "text/csv": icons.csv, "audio/aac": icons.audio, "audio/wav": icons.audio, "audio/mpeg": icons.audio, "audio/mp4": icons.audio, "audio/ogg": icons.audio, "video/x-msvideo": icons.video, "video/mpeg": icons.video, "video/mp4": icons.video, "video/ogg": icons.video, "video/quicktime": icons.video, "video/webm": icons.video, "application/gzip": icons.archive, "application/zip": icons.archive, "application/x-tar": icons.archive, "text/css": icons.code, "text/html": icons.code, "text/javascript": icons.code, "application/javascript": icons.code, "text/plain": icons.text, "text/richtext": icons.text, "text/rtf": icons.text, "application/rtf": icons.text, "application/json": icons.text }; const extensions = { gif: icons.image, jpeg: icons.image, jpg: icons.image, png: icons.image, webp: icons.image, pdf: icons.pdf, doc: icons.word, docx: icons.word, ppt: icons.powerpoint, pptx: icons.powerpoint, xls: icons.excel, xlsx: icons.excel, csv: icons.csv, aac: icons.audio, mp3: icons.audio, ogg: icons.audio, avi: icons.video, flv: icons.video, mkv: icons.video, mp4: icons.video, gz: icons.archive, zip: icons.archive, tar: icons.archive, "7z": icons.archive, css: icons.code, html: icons.code, js: icons.code, txt: icons.text, json: icons.text, rtf: icons.text }; export default { /////////////////////////////// // CLEAN OBJECT // Clear all properties from object without resorting to assigning a new object (o={}) // which can be problematic in some cases (IE bugs, watched data items in forms etc) removeAllPropertiesFromObject: function(o) { for (let variableKey in o) { if (Object.prototype.hasOwnProperty.call(o, variableKey)) { delete o[variableKey]; } } }, /////////////////////////////// // DEEP COPY FOR API UPDATE // Deep copy an object skipping all *Viz and named properties from object // deepCopySkip: function(source, skipNames) { if (skipNames == null) { skipNames = []; } let o = {}; for (let key in source) { if ( !key.endsWith("Viz") && !skipNames.some(x => x == key) && Object.prototype.hasOwnProperty.call(source, key) ) { o[key] = source[key]; } } return o; }, /** * Copy a string to clipboard * @param {String} string The string to be copied to clipboard * @return {Boolean} returns a boolean correspondent to the success of the copy operation. * Modified from an example here: https://stackoverflow.com/a/53951634/8939 * Basically a fallback if navigator.clipboard is not available */ copyToClipboard: function(string) { let textarea; let result; if (navigator && navigator.clipboard) { navigator.clipboard.writeText(string); } else { try { textarea = document.createElement("textarea"); textarea.setAttribute("readonly", true); textarea.setAttribute("contenteditable", true); textarea.style.position = "fixed"; // prevent scroll from jumping to the bottom when focus is set. textarea.value = string; document.body.appendChild(textarea); textarea.focus(); textarea.select(); const range = document.createRange(); range.selectNodeContents(textarea); const sel = window.getSelection(); sel.removeAllRanges(); sel.addRange(range); textarea.setSelectionRange(0, textarea.value.length); result = document.execCommand("copy"); } catch (err) { result = null; } finally { document.body.removeChild(textarea); } // manual copy fallback using prompt if (!result) { const isMac = navigator.platform.toUpperCase().indexOf("MAC") >= 0; const copyHotkey = isMac ? "⌘C" : "CTRL+C"; result = prompt(`Press ${copyHotkey}`, string); if (!result) { return false; } } } return true; }, /////////////////////////////// // ROUNDING // //https://medium.com/swlh/how-to-round-to-a-certain-number-of-decimal-places-in-javascript-ed74c471c1b8 roundAccurately: function(number, decimalPlaces) { if (!number || number == 0 || Number.isNaN(number)) { return number; } const wasNegative = number < 0; if (wasNegative) { number = Math.abs(number); //make sure it's positive because rounding negative numbers is weird in JS } number = Number( Math.round(number + "e" + decimalPlaces) + "e-" + decimalPlaces ); if (wasNegative) { number = 0 - number; } return number; }, /////////////////////////////// // CLEAN TAG NAME // Clean up a tag with same rules as server // normalizeTag: function(tagName) { if (!tagName || tagName == "") { return null; } tagName = tagName.toLowerCase(); //spaces to dashes tagName = tagName.replace(/ /gi, "-"); //multiple dashes to single dashes tagName = tagName.replace(/-+/g, "-"); //ensure doesn't start or end with a dash tagName = this.trimSpecific(tagName, "-"); //No longer than 255 characters tagName = tagName.length > 255 ? tagName.substr(0, 255 - 1) : tagName; return tagName; }, /////////////////////////////// // Quick hash for trivial purposes // not cryptographic // https://stackoverflow.com/a/7616484/8939 // quickHash: function(theString) { let hash = 0; let i; let chr; if (theString.length === 0) return hash; for (i = 0; i < theString.length; i++) { chr = theString.charCodeAt(i); hash = (hash << 5) - hash + chr; hash |= 0; // Convert to 32bit integer } return hash; }, //////////////////////////////////////// // Random password / login generator // https://stackoverflow.com/a/51540480/8939 // using 32 character (128 bit) as default // getRandomPassword: function() { const wishlist = "0123456789abcdefghijkmnopqrstuvwxyz"; return Array.from(crypto.getRandomValues(new Uint32Array(32))) .map(x => wishlist[x % wishlist.length]) .join(""); }, /////////////////////////////// // CONVERT STRING TO BOOLEAN // https://stackoverflow.com/a/1414175/8939 // stringToBoolean: function(string) { switch (string.toLowerCase().trim()) { case "true": case "yes": case "1": return true; case "false": case "no": case "0": case null: return false; default: return Boolean(string); } }, /////////////////////////////// // CONVERT STRING TO FLOAT // https://stackoverflow.com/a/9409894/8939 // stringToFloat: function(string) { //null or empty then zero if (!string) { return 0; } //A number already then parse and return if (this.isNumeric(string)) { if (Number.isNaN(string)) { return 0; } return parseFloat(string); } //Not a string at all? if (!this.isString(string)) { return 0; } const ret = parseFloat(string.replace(/[^\d.-]/g, "")); if (Number.isNaN(ret)) { return 0; } return ret; }, /////////////////////////////// // Is negative number // // isNegative: function(v) { //null or empty then zero if (!v || v == 0 || Number.isNaN(v)) { return false; } return parseFloat(v) < 0; }, /////////////////////////////// // Splice a string //changes the content of a string by removing a range of // characters and/or adding new characters. // // @param {String} source string // @param {number} start Index at which to start changing the string. // @param {number} delCount An integer indicating the number of old chars to remove. // @param {string} newSubStr The String that is spliced in. // @return {string} A new string with the spliced substring. stringSplice: function(source, start, delCount, newSubStr) { if (source == null || source == "") { if (newSubStr) { return newSubStr; } return ""; } return ( source.slice(0, start) + newSubStr + source.slice(start + Math.abs(delCount)) ); }, /////////////////////////////// // Truncate a string //truncates and adds ellipses // // @param {String} source string // @param {number} length desired // @return {string} A new string truncated with ellipses at end truncateString: function(s, len) { if (this.stringIsNullOrEmpty(s)) { return s; } if (s.length > len) { return s.substring(0, len) + "..."; } else { return s; } }, /////////////////////////////// // Format tags for display // // // @param {String} tags raw from server // @return {string} A new string with the tags formatted or an empty string if no tags formatTags: function(tags) { if (tags && tags.length > 0) { return tags.join(", "); } return ""; }, /////////////////////////////// // ICON FOR *ALL* OBJECT TYPES //(used for search results and event log / history) //NOTE: Any object type could appear in event log, they all need to be supported where possible //CoreBizObject add here iconForType: function(ayaType) { switch (ayaType) { case window.$gz.type.NoType: case null: return "$ayiGenderless"; case window.$gz.type.Global: return "$ayiGlobe"; case window.$gz.type.User: return "$ayiUser"; case window.$gz.type.ServerState: return "$ayiDoorOpen"; case window.$gz.type.License: return "$ayiTicket"; case window.$gz.type.LogFile: return "$ayiGlasses"; case window.$gz.type.PickListTemplate: return "$ayiPencilRuler"; case window.$gz.type.Customer: return "$ayiAddressCard"; case window.$gz.type.ServerJob: return "$ayiRobot"; case window.$gz.type.Contract: return "$ayiFileContract"; case window.$gz.type.TrialSeeder: return "$ayiSeedling"; case window.$gz.type.Metrics: return "$ayiFileMedicalAlt"; case window.$gz.type.Translation: return "$ayiLanguage"; case window.$gz.type.UserOptions: return "$ayiUserCog"; case window.$gz.type.HeadOffice: return "$ayiSitemap"; case window.$gz.type.LoanUnit: return "$ayiPlug"; case window.$gz.type.FileAttachment: return "$ayiPaperclip"; case window.$gz.type.DataListSavedFilter: return "$ayiFilter"; case window.$gz.type.FormCustom: return "$ayiCustomize"; case window.$gz.type.Part: return "$ayiBoxes"; case window.$gz.type.PartWarehouse: return "$ayiWarehouse"; case window.$gz.type.PartAssembly: return "$ayiObjectGroup"; case window.$gz.type.Project: return "$ayiProjectDiagram"; case window.$gz.type.PurchaseOrder: return "$ayiTruckLoading"; case window.$gz.type.Unit: return "$ayiFan"; case window.$gz.type.UnitModel: return "$ayiDiceD20"; case window.$gz.type.Vendor: return "$ayiStore"; case window.$gz.type.Quote: return "$ayiPencilAlt"; case window.$gz.type.PM: return "$ayiBusinessTime"; case window.$gz.type.WorkOrder: return "$ayiTools"; case window.$gz.type.WorkOrderItem: case window.$gz.type.PMItem: case window.$gz.type.QuoteItem: return "$ayiWrench"; case window.$gz.type.WorkOrderItemExpense: case window.$gz.type.QuoteItemExpense: case window.$gz.type.PMItemExpense: return "$ayiMoneyBillWave"; case window.$gz.type.WorkOrderItemLabor: case window.$gz.type.QuoteItemLabor: case window.$gz.type.PMItemLabor: return "$ayiHammer"; case window.$gz.type.WorkOrderItemLoan: case window.$gz.type.QuoteItemLoan: case window.$gz.type.PMItemLoan: return "$ayiPlug"; case window.$gz.type.WorkOrderItemPart: case window.$gz.type.QuoteItemPart: case window.$gz.type.PMItemPart: return "$ayiBoxes"; case window.$gz.type.WorkOrderItemPartRequest: return "$ayiParachuteBox"; case window.$gz.type.WorkOrderItemScheduledUser: case window.$gz.type.QuoteItemScheduledUser: case window.$gz.type.PMItemScheduledUser: return "$ayiUserClock"; case window.$gz.type.WorkOrderItemTask: case window.$gz.type.QuoteItemTask: case window.$gz.type.PMItemTask: case window.$gz.type.TaskGroup: return "$ayiTasks"; case window.$gz.type.WorkOrderItemTravel: case window.$gz.type.QuoteItemTravel: case window.$gz.type.PMItemTravel: return "$ayiTruckMonster"; case window.$gz.type.WorkOrderItemUnit: case window.$gz.type.QuoteItemUnit: case window.$gz.type.PMItemUnit: return "$ayiFan"; case window.$gz.type.WorkOrderItemOutsideService: case window.$gz.type.QuoteItemOutsideService: case window.$gz.type.PMItemOutsideService: return "$ayiLuggageCart"; case window.$gz.type.Backup: return "$ayiFileArchive"; case window.$gz.type.Notification: return "$ayiBell"; case window.$gz.type.NotifySubscription: return "$ayiBullhorn"; case window.$gz.type.Reminder: return "$ayiStickyNote"; case window.$gz.type.UnitMeterReading: return "$ayiWeight"; case window.$gz.type.CustomerServiceRequest: return "$ayiConciergeBell"; // case window.$gz.type.ServiceBank: // return "$ayiCarBattery"; case window.$gz.type.OpsNotificationSettings: return "$ayiBullhorn"; case window.$gz.type.Report: return "$ayiThList"; case window.$gz.type.DashboardView: return "$ayiTachometer"; case window.$gz.type.CustomerNote: return "$ayiClipboard"; case window.$gz.type.Memo: return "$ayiInbox"; case window.$gz.type.Review: return "$ayiCalendarCheck"; case window.$gz.type.ServiceRate: return "$ayiCalculator"; case window.$gz.type.TravelRate: return "$ayiCalculator"; case window.$gz.type.TaxCode: return "$ayiPercent"; case window.$gz.type.WorkOrderStatus: return "$ayiFlag"; //scroll icon is good one for something default: return null; } }, //https://gist.github.com/colemanw/9c9a12aae16a4bfe2678de86b661d922 iconForFile: function(fileName, mimeType) { // List of official MIME Types: http://www.iana.org/assignments/media-types/media-types.xhtml let extension = null; if (fileName && fileName.includes(".")) { extension = fileName.split(".").pop(); extension = extension.toLowerCase(); } if (!extension && !mimeType) { console.log( "gzutil:iconForFile -> No mime or extension for " + fileName + " " + mimeType ); return "$ayiFile"; } if (!mimeType) { mimeType = ""; } mimeType = mimeType.toLowerCase(); const iconFromExtension = extensions[extension]; const iconFromMIME = mimeTypes[mimeType]; if (iconFromMIME) { return iconFromMIME; } if (iconFromExtension) { return iconFromExtension; } return "$ayiFile"; }, /////////////////////////////////////////////// // attempt to detect image extension name // isImageAttachment: function(fileName, mimeType) { return this.iconForFile(fileName, mimeType) == "$ayiFileImage"; }, /////////////////////////////////////////////// // Sleep async // sleepAsync: function(milliseconds) { // eslint-disable-next-line return new Promise((resolve) => setTimeout(resolve, milliseconds)); }, /////////////////////////////////////////////// // sortByKey lodash "sortBy" replacement // https://github.com/you-dont-need/You-Dont-Need-Lodash-Underscore#_sortby-and-_orderby //usage: // The native sort modifies the array in place. `_.orderBy` and `_.sortBy` do not, so we use `.concat()` to // copy the array, then sort. // fruits.concat().sort(sortBy("name")); // => [{name:"apple", amount: 4}, {name:"banana", amount: 2}, {name:"mango", amount: 1}, {name:"pineapple", amount: 2}] sortByKey: key => { return (a, b) => { const aaa = a[key].toUpperCase(); const bbb = b[key].toUpperCase(); return aaa > bbb ? 1 : bbb > aaa ? -1 : 0; //this was the original but it was sorting weird as it was taking case into account with uppercase higher than lowercase //so PMItem came before Part in the object lists //return a[key] > b[key] ? 1 : b[key] > a[key] ? -1 : 0; }; }, /////////////////////////////////////////////// // "has" lodash replacement // https://github.com/you-dont-need/You-Dont-Need-Lodash-Underscore#_has // has: function(obj, key) { var keyParts = key.split("."); return ( !!obj && (keyParts.length > 1 ? this.has(obj[key.split(".")[0]], keyParts.slice(1).join(".")) : hasOwnProperty.call(obj, key)) ); }, /////////////////////////////////////////////// // Check if object is empty // objectIsEmpty: function(obj) { //https://stackoverflow.com/a/4994265/8939 return !obj || Object.keys(obj).length === 0; }, /////////////////////////////////////////////// // Trim specific character from start and end // https://stackoverflow.com/a/55292366/8939 // trimSpecific: function trim(str, ch) { var start = 0; var end = str.length; while (start < end && str[start] === ch) ++start; while (end > start && str[end - 1] === ch) --end; return start > 0 || end < str.length ? str.substring(start, end) : str; }, /////////////////////////////////////////////// // is numeric replacement for lodash // https://stackoverflow.com/a/52986361/8939 // isNumeric: function(n) { //lodash isNumber returned false if it's a string and that's what the rest of the code expects even though it's parseable to a number return !this.isString(n) && !isNaN(parseFloat(n)) && isFinite(n); }, /////////////////////////////////////////////// // is string replacement for lodash // https://github.com/you-dont-need/You-Dont-Need-Lodash-Underscore#_isString // isString: function(str) { return str != null && typeof str.valueOf() === "string"; }, /////////////////////////////////////////////// // // // stringIsNullOrEmpty: function(str) { if (str === null || str === undefined) { return true; } if (this.isString(str)) { if (str.trim() == "") { return true; } } return false; }, /////////////////////////////////////////////// // is Boolean replacement for lodash // https://stackoverflow.com/a/43718478/8939 // isBoolean: function(obj) { return obj === true || obj === false || typeof variable === "boolean"; }, /////////////////////////////////////////////// // parse to number or null if not a number // used because route params can turn into strings // on their own // stringToIntOrNull: function(n) { const ret = Number.parseInt(n, 10); if (Number.isNaN(ret)) { return null; } return ret; }, /////////////////////////////////////////////// // Simple array equality comparison // (will NOT work on arrays of objects) // Array order is relevant here as they are not sorted // change of order will equal change of array // as this is required for datatable sortby // isEqualArraysOfPrimitives: function(a, b) { if (a === b) return true; if (a == null || b == null) return false; if (a.length !== b.length) return false; // If you don't care about the order of the elements inside // the array, you should sort both arrays here. // Please note that calling sort on an array will modify that array. // you might want to clone your array first. for (var i = 0; i < a.length; ++i) { if (a[i] !== b[i]) return false; } return true; }, /////////////////////////////////////////////// // Use geolocation api to attempt to get current location // try high accuracy first and downgrade if unavailable //https://www.openstreetmap.org/?mlat=48.3911&mlon=-124.7353#map=12/48.3910/-124.7353 //https://www.openstreetmap.org/#map=18/49.68155/-125.00435 //https://www.openstreetmap.org/?mlat=49.71236&mlon=-124.96961#map=17/49.71236/-124.96961 //https://www.google.com/maps/search/?api=1&query=47.5951518,-122.3316393 getGeoLocation: async function() { return new Promise((resolve, reject) => { navigator.geolocation.getCurrentPosition( function successHigh(pos) { resolve({ latitude: pos.coords.latitude, longitude: pos.coords.longitude }); }, function error(err) { //if here due to timeout getting high accuracy then try again with low accuracy if (error.code == error.TIMEOUT) { navigator.geolocation.getCurrentPosition( function successLow(pos) { resolve({ latitude: pos.coords.latitude, longitude: pos.coords.longitude }); }, function error(err) { reject( new Error( `ERROR getting location(low_accuracy: ${err.code}): ${err.message}` ) ); }, { maximumAge: 600000, timeout: 10000, enableHighAccuracy: false } ); return; } reject( new Error( `ERROR GETTING LOCATION(high_accuracy:${err.code}): ${err.message}` ) ); }, { maximumAge: 600000, timeout: 5000, enableHighAccuracy: true } ); }); }, /////////////////////////////////////////////// // Open map url // // viewGeoLocation: function(obj) { const hasGeo = obj.latitude != null && obj.latitude != 0 && obj.longitude != null && obj.longitude != 0; const hasAddress = !this.stringIsNullOrEmpty(obj.address) && !this.stringIsNullOrEmpty(obj.city) && !this.stringIsNullOrEmpty(obj.region) && !this.stringIsNullOrEmpty(obj.country) && !this.stringIsNullOrEmpty(obj.postCode); if (!hasGeo && !hasAddress) { return; } let mapUrl = window.$gz.store.state.userOptions.mapUrlTemplate; //No pre-set? if (!mapUrl || mapUrl == "") { mapUrl = "https://www.google.com/maps/search/?api=1&query={ayaddress}<|>https://www.google.com/maps/search/?api=1&query={aylatitude},{aylongitude}"; } let geoMapUrl = null; let addressMapUrl = null; //Parse the map url let mapUrls = [mapUrl]; if (mapUrl.includes("<|>")) { mapUrls = mapUrl.split("<|>"); } mapUrls.forEach(z => { if (!geoMapUrl && z.includes("{aylatitude}")) { geoMapUrl = z; } if (!addressMapUrl && z.includes("{ayaddress}")) { addressMapUrl = z; } }); //decide which map to use here, favor geocode if (hasGeo && geoMapUrl) { //geo view mapUrl = geoMapUrl; mapUrl = mapUrl.split("{aylatitude}").join(obj.latitude); mapUrl = mapUrl.split("{aylongitude}").join(obj.longitude); } else if (hasAddress && addressMapUrl) { mapUrl = addressMapUrl; //compile address fields together //order street to country seems to be standard //note, if google need plus symbol delimiter, if bing, need comma delimiter //but both might accept one big string space delimited and url encoded so test that on all first const delimiter = " "; let q = ""; if (obj.address) { q += obj.address + delimiter; } if (obj.city) { q += obj.city + delimiter; } if (obj.region) { q += obj.region + delimiter; } if (obj.country) { q += obj.country + delimiter; } if (obj.postCode) { q += obj.postCode + delimiter; } if (obj.addressPostal) { q += obj.addressPostal + delimiter; } if (q.length > 1) { q = q.substring(0, q.length - 1); } //url encode the query q = encodeURIComponent(q); mapUrl = mapUrl.split("{ayaddress}").join(q); } else { throw new Error( "View map: error - no matching mapurl / address / geo coordinates set for display, nothing to view" ); } window.open(mapUrl, "map"); //This is not valid to do as some platforms don't open a new web browser window //but rather a map application in which case this is null and throws up the exception even though it's working // if (window.open(mapUrl, "map") == null) { // throw new Error( // "Problem displaying map in new window. Browser must allow pop-ups to view maps; check your browser setting" // ); // } }, /////////////////////////////////////////////// // Online mapping service url formats // // mapProviderUrls: function() { return [ { name: "Apple", value: "http://maps.apple.com/?q={ayaddress}<|>http://maps.apple.com/?ll={aylatitude},{aylongitude}" }, { name: "Bing", value: "https://bing.com/maps/default.aspx?where1={ayaddress}<|>https://bing.com/maps/default.aspx?cp={aylatitude}~{aylongitude}&lvl=17&style=r&sp=point.{aylatitude}_{aylongitude}" }, { name: "Google", value: "https://www.google.com/maps/search/?api=1&query={ayaddress}<|>https://www.google.com/maps/search/?api=1&query={aylatitude},{aylongitude}" }, { name: "MapQuest", value: "https://mapquest.com/?center={ayaddress}&zoom=17<|>https://mapquest.com/?center={aylatitude},{aylongitude}&zoom=17" }, { name: "Open Street Map", value: "https://www.openstreetmap.org/search?query={ayaddress}<|>https://www.openstreetmap.org/?mlat={aylatitude}&mlon={aylongitude}#map=17/{aylatitude}/{aylongitude}" }, { name: "geo URI", value: "geo:{aylatitude},{aylongitude}" }, { name: "Waze", value: "https://waze.com/ul?q={ayaddress}<|>https://www.waze.com/ul?ll={aylatitude},{aylongitude}&navigate=yes&zoom=17" }, { name: "Yandex", value: "https://yandex.ru/maps/?mode=search&text={ayaddress}&z=17<|>https://yandex.ru/maps/?ll={aylatitude},{aylongitude}&z=12&l=map" } ]; }, /////////////////////////////////////////////// // v-calendar view to AyaNova scheduleview enum // // calendarViewToAyaNovaEnum: function(view) { switch (view) { case "day": return 1; case "week": return 2; case "month": return 3; case "4day": return 4; case "category": return 5; default: throw new Error( `gzutil->calendarViewtoAyaNovaEnum - Unknown view type '${view}'` ); } }, /////////////////////////////////////////////// // GZDaysOfWeek to VCalendar weekdays // // DaysOfWeekToWeekdays: function(dow) { /* AyaDaysOfWeek Monday = 1, Tuesday = 2, Wednesday = 4, Thursday = 8, Friday = 16, Saturday = 32, Sunday = 64 vCalendar [ 0,//sunday 1, 2, 3, 4, 5, 6//saturday ] */ if (dow == null || dow == 0) { return [0, 1, 2, 3, 4, 5, 6]; //all the days } const ret = []; //turn EXCLUDE selected gzDaysOfWeek into INCLUDE selected days for vCalendar if (!(dow & 64)) { ret.push(0); } if (!(dow & 1)) { ret.push(1); } if (!(dow & 2)) { ret.push(2); } if (!(dow & 4)) { ret.push(3); } if (!(dow & 8)) { ret.push(4); } if (!(dow & 16)) { ret.push(5); } if (!(dow & 32)) { ret.push(6); } return ret; }, /////////////////////////////////////////////// // Random integer from 0 to max // // getRandomInt: function(max) { return Math.floor(Math.random() * max); } /** * * */ //new functions above here };