From ab122fb82941b06187cc3555a049dcd4187c8ca3 Mon Sep 17 00:00:00 2001 From: Peter Joles Date: Sat, 22 Feb 2020 19:25:43 -0800 Subject: [PATCH] :children_crossing: Improving UX for sharable imports of solutions --- src/App.vue | 59 +- src/utils/utils.js | 1835 ++++++++++++++------------- src/views/Config.vue | 2869 +++++++++++++++++++++--------------------- 3 files changed, 2416 insertions(+), 2347 deletions(-) diff --git a/src/App.vue b/src/App.vue index d0bbb77e..2bdf8dbb 100644 --- a/src/App.vue +++ b/src/App.vue @@ -407,13 +407,14 @@

Accepting will overwrite other solutions with the same name or deep link. + >You already have a solution with the same name or deep link. @@ -459,7 +475,15 @@ const logger = require("@/utils/logging").getLogger("App.vue"); import "wicg-inert/dist/inert.min"; import { mapGetters } from "vuex"; import { STORAGE_KEY } from "@/constants/solution-config-default"; -import { debounce, sendMessageToParent, isLight, isDark } from "@/utils/utils"; +import { + debounce, + sendMessageToParent, + isLight, + isDark, + hasConflictingSolution, + makeSolutionUnique, + uuid +} from "@/utils/utils"; import "prismjs/prism"; import "prismjs/themes/prism-coy.css"; import "prismjs/components/prism-json.min"; @@ -599,6 +623,11 @@ export default { this.importedSolution = jsonpack.unpack(solConfig); this.importDialogMessages.message = `Do you want to import this solution?`; this.importDialogMessages.solution = this.importedSolution; + this.importDialogMessages.hasConflictingSolution = hasConflictingSolution( + this.importedSolution, + this.config + ); + logger.debug(`Importing the following solution config`, solConfig); this.importDialog = true; } @@ -849,7 +878,25 @@ export default { } return true; }, - importSolutionFromUrl() { + importNewSolutionFromUrl() { + this.importDialog = false; + this.importedSolution.id = uuid(); + this.config.solutions.push(this.importedSolution); // no conflicts + let deepLinkUrl = `${location.protocol}//${location.host}${location.pathname}?dl=${this.importedSolution.deepLink}`; + localStorage.setItem(STORAGE_KEY + "config", JSON.stringify(this.config)); + logger.debug(`Deep Link Url: `, deepLinkUrl); + window.location.href = deepLinkUrl; + }, + importNewUniqueSolutionFromUrl() { + this.importDialog = false; + this.importSolution = makeSolutionUnique(this.importedSolution); + this.config.solutions.push(this.importedSolution); // no conflicts + let deepLinkUrl = `${location.protocol}//${location.host}${location.pathname}?dl=${this.importedSolution.deepLink}`; + localStorage.setItem(STORAGE_KEY + "config", JSON.stringify(this.config)); + logger.debug(`Deep Link Url: `, deepLinkUrl); + window.location.href = deepLinkUrl; + }, + importReplacementSolutionFromUrl() { this.importDialog = false; let existingSolutionsWithName = this.config.solutions.findIndex( @@ -873,7 +920,7 @@ export default { } this.config.solutions.push(this.importedSolution); } - this.config.activeSolution = this.importedSolution.name; + this.config.activeSolution = this.importedSolution.id; let deepLinkUrl = `${location.protocol}//${location.host}${location.pathname}?dl=${this.importedSolution.deepLink}`; localStorage.setItem(STORAGE_KEY + "config", JSON.stringify(this.config)); logger.debug(`Deep Link Url: `, deepLinkUrl); diff --git a/src/utils/utils.js b/src/utils/utils.js index 068a11cc..a841dade 100644 --- a/src/utils/utils.js +++ b/src/utils/utils.js @@ -1,913 +1,932 @@ -/* eslint-disable func-names */ -/* eslint-disable no-plusplus */ -/* eslint-disable guard-for-in */ -/* eslint-disable no-restricted-syntax */ -/* eslint-disable no-bitwise */ -/* eslint-disable no-useless-escape */ -/* eslint-disable no-unused-vars */ - -const replaceString = require("replace-string"); -const solutionDefault = require("@/constants/solution-config-default").SOLUTION_DEFAULT; -const jsonpack = require("jsonpack/main"); -const uuidv4 = require("uuid/v4"); - -export const uuid = () => uuidv4(); - -export const fixSolution = solution => { - if (!("id" in solution)) { - const id = uuid(); - // TODO: I know I need to fix this... - // if (solution.name === allSolutions.activeSolution) { - // allSolutions.activeSolution = id; - // } - solution.id = id; - } - if (!("responseDelay" in solution)) { - solution.responseDelay = 0; - } - if (!("font" in solution)) { - solution.font = solutionDefault.font; - } - if (!("lookAndFeel" in solution)) { - solution.lookAndFeel = solutionDefault.lookAndFeel; - } - - if (!("custom1" in solution.theme)) { - solution.theme.dark = solutionDefault.theme.dark; - solution.theme.custom1 = solutionDefault.theme.custom1; - solution.theme.custom2 = solutionDefault.theme.custom2; - solution.theme.custom3 = solutionDefault.theme.custom3; - } - - if (!("focusButton" in solution.theme)) { - solution.theme.focusButton = solutionDefault.theme.focusButton; - } - - if (!("sendButton" in solution.theme)) { - solution.theme.sendButton = solution.theme.primary; - } - - if (!("textButton" in solution.theme)) { - solution.theme.textButton = solutionDefault.theme.textButton; - } - - if (!("animations" in solution)) { - solution.animations = solutionDefault.animations; - } - if (!("promptTriggers" in solution)) { - solution.promptTriggers = solutionDefault.promptTriggers; - } - return solution; -}; - -const WHITE_SPACES = [ - " ", - "\n", - "\r", - "\t", - "\f", - "\v", - "\u00A0", - "\u1680", - "\u180E", - "\u2000", - "\u2001", - "\u2002", - "\u2003", - "\u2004", - "\u2005", - "\u2006", - "\u2007", - "\u2008", - "\u2009", - "\u200A", - "\u2028", - "\u2029", - "\u202F", - "\u205F", - "\u3000" -]; - -export const fixSolutions = allSolutions => { - let origChatConfig = JSON.stringify(allSolutions); - origChatConfig = replaceString(origChatConfig, '"true"', "true"); - origChatConfig = replaceString(origChatConfig, '"false"', "false"); - allSolutions = JSON.parse(origChatConfig); - - if ("solutions" in allSolutions) { - allSolutions.solutions.forEach(solution => { - solution = fixSolution(solution); - }); - } else if ("url" in allSolutions) { - // not really a solutions file rather just a solution - const solutionsWrapper = { - activeSolution: "", - solutions: [] - }; - const fixedSolution = fixSolution(allSolutions); - solutionsWrapper.activeSolution = fixedSolution.id; - solutionsWrapper.solutions.push(fixedSolution); - allSolutions = solutionsWrapper; - } - - return allSolutions; -}; - -export const parseExtraData = input => { - let result = null; - if (input) { - try { - result = decodeURIComponent(input); - } catch (e) { - result = input; - } - try { - result = JSON.parse(result); - } catch (e) { - result = null; - } - } - return result; -}; - -/** - * Remove chars from beginning of string. - */ -export const ltrim = (str, chars) => { - chars = chars || WHITE_SPACES; - - let start = 0; - const len = str.length; - const charLen = chars.length; - let found = true; - let i; - let c; - - while (found && start < len) { - found = false; - i = -1; - c = str.charAt(start); - - while (++i < charLen) { - if (c === chars[i]) { - found = true; - start++; - break; - } - } - } - - return start >= len ? "" : str.substr(start, len); -}; - -/** - * Remove chars from end of string. - */ -export const rtrim = (str, chars) => { - chars = chars || WHITE_SPACES; - - let end = str.length - 1; - const charLen = chars.length; - let found = true; - let i; - let c; - - while (found && end >= 0) { - found = false; - i = -1; - c = str.charAt(end); - - while (++i < charLen) { - if (c === chars[i]) { - found = true; - end--; - break; - } - } - } - - return end >= 0 ? str.substring(0, end + 1) : ""; -}; - -/** - * Remove white-spaces from beginning and end of string. - */ -export const trim = (str, chars) => { - chars = chars || WHITE_SPACES; - return ltrim(rtrim(str, chars), chars); -}; - -/** - * Limit number of chars. - */ -export const truncate = (str, maxChars, append, onlyFullWords) => { - append = append || "..."; - maxChars = onlyFullWords ? maxChars + 1 : maxChars; - - str = trim(str); - if (str.length <= maxChars) { - return str; - } - str = str.substr(0, maxChars - append.length); - // crop at last space or remove trailing whitespace - str = onlyFullWords ? str.substr(0, str.lastIndexOf(" ")) : trim(str); - return str + append; -}; - -export const lightOrDark = color => { - // Variables for red, green, blue values - let r; - let g; - let b; - - // Check the format of the color, HEX or RGB? - if (color.match(/^rgb/)) { - // If HEX --> store the red, green, blue values in separate variables - color = color.match(/^rgba?\((\d+),\s*(\d+),\s*(\d+)(?:,\s*(\d+(?:\.\d+)?))?\)$/); - - r = color[1]; - g = color[2]; - b = color[3]; - } else { - // If RGB --> Convert it to HEX: http://gist.github.com/983661 - color = +`0x${color.slice(1).replace(color.length < 5 && /./g, "$&$&")}`; - - r = color >> 16; - g = (color >> 8) & 255; - b = color & 255; - } - - // HSP (Highly Sensitive Poo) equation from http://alienryderflex.com/hsp.html - const hsp = Math.sqrt(0.299 * (r * r) + 0.587 * (g * g) + 0.114 * (b * b)); - - // Using the HSP value, determine whether the color is light or dark 127.5 orig - if (hsp > 145) { - // console.log("Light >> HSP: ", hsp); - return "light"; - } - // console.log("Dark >> HSP: ", hsp); - return "dark"; -}; - -export const isEmpty = obj => { - for (const key in obj) { - return false; - } - return true; -}; - -export const isLight = color => { - if (lightOrDark(color) === "light") { - return true; - } - return false; -}; - -export const isDark = color => { - if (lightOrDark(color) === "dark") { - return true; - } - return false; -}; - -export const sendMessageToParent = message => { - if (window.parent) { - window.parent.postMessage(message, "*"); // post multiple times to each domain you want leopard on. Specifiy origin for each post. - } -}; - -export const replaceAll = (targetStr, findStr, replaceStr = "") => { - return targetStr.split(findStr).join(replaceStr); -}; - -export const removeAll = (targetStr, findArr) => { - findArr.forEach(find => { - targetStr = replaceAll(targetStr, find); - }); - return targetStr; -}; - -export function debounce(func, wait, immediate) { - let timeout; - return function debounceLogic(...args) { - const context = this; - clearTimeout(timeout); - timeout = setTimeout(function callFunction() { - timeout = null; - if (!immediate) func.apply(context, args); - }, wait); - if (immediate && !timeout) func.apply(context, args); - }; -} - -/** - * Smooth scroll - */ -// easing functions http://goo.gl/5HLl8 -Math.easeInOutQuad = function(t, b, c, d) { - t /= d / 2; - if (t < 1) { - return (c / 2) * t * t + b; - } - t--; - return (-c / 2) * (t * (t - 2) - 1) + b; -}; - -Math.easeInCubic = function(t, b, c, d) { - const tc = (t /= d) * t * t; - return b + c * tc; -}; - -Math.inOutQuintic = function(t, b, c, d) { - const ts = (t /= d) * t; - const tc = ts * t; - return b + c * (6 * tc * ts + -15 * ts * ts + 10 * tc); -}; - -// requestAnimationFrame for Smart Animating http://goo.gl/sx5sts -export const requestAnimFrame = (function() { - return ( - window.requestAnimationFrame || - window.webkitRequestAnimationFrame || - window.mozRequestAnimationFrame || - function(callback) { - window.setTimeout(callback, 1000 / 60); - } +/* eslint-disable func-names */ +/* eslint-disable no-plusplus */ +/* eslint-disable guard-for-in */ +/* eslint-disable no-restricted-syntax */ +/* eslint-disable no-bitwise */ +/* eslint-disable no-useless-escape */ +/* eslint-disable no-unused-vars */ + +const replaceString = require("replace-string"); +const solutionDefault = require("@/constants/solution-config-default").SOLUTION_DEFAULT; +const jsonpack = require("jsonpack/main"); +const uuidv4 = require("uuid/v4"); + +export const uuid = () => uuidv4(); + +export const hasConflictingSolution = (solution, allSolutions) => { + let foundSolution = allSolutions.solutions.find( + existingSol => existingSol.name === solution.name || existingSol.deepLink === solution.deepLink ); -})(); - -export const scrollTo = (to, callback, duration) => { - // because it's so fucking difficult to detect the scrolling element, just move them all - function move(amount) { - document.documentElement.scrollTop = amount; - document.body.parentNode.scrollTop = amount; - document.body.scrollTop = amount; - } - function position() { - return ( - document.documentElement.scrollTop || - document.body.parentNode.scrollTop || - document.body.scrollTop - ); - } - const start = position(); - const change = to - start; - let currentTime = 0; - const increment = 20; - duration = typeof duration === "undefined" ? 500 : duration; - const animateScroll = function() { - // increment the time - currentTime += increment; - // find the value with the quadratic in-out easing function - const val = Math.easeInOutQuad(currentTime, start, change, duration); - // move the document.body - move(val); - // do the animation unless its over - if (currentTime < duration) { - requestAnimFrame(animateScroll); - } else if (callback && typeof callback === "function") { - // the animation is done so lets callback - callback(); - } - }; - animateScroll(); -}; -// end smooth scroll - -export const cleanEmptyChunks = answerText => { - let finalAnswerText = ""; - const chunks = answerText.split("||"); - chunks.forEach(chunk => { - const trimmedChunk = chunk.trim(); - if (trimmedChunk) { - finalAnswerText += `||${trimmedChunk}`; - } - }); - if (finalAnswerText.startsWith("||")) { - finalAnswerText = finalAnswerText.substring(2); - } - return finalAnswerText.trim(); -}; - -export const lowerCase = str => { - return str.toLowerCase(); -}; - -/** - * "Safer" String.toUpperCase() - */ -export const upperCase = str => { - return str.toUpperCase(); -}; - -/** - * Replaces all accented chars with regular ones - */ -export const replaceAccents = str => { - // verifies if the String has accents and replace them - if (str.search(/[\xC0-\xFF]/g) > -1) { - str = str - .replace(/[\xC0-\xC5]/g, "A") - .replace(/[\xC6]/g, "AE") - .replace(/[\xC7]/g, "C") - .replace(/[\xC8-\xCB]/g, "E") - .replace(/[\xCC-\xCF]/g, "I") - .replace(/[\xD0]/g, "D") - .replace(/[\xD1]/g, "N") - .replace(/[\xD2-\xD6\xD8]/g, "O") - .replace(/[\xD9-\xDC]/g, "U") - .replace(/[\xDD]/g, "Y") - .replace(/[\xDE]/g, "P") - .replace(/[\xE0-\xE5]/g, "a") - .replace(/[\xE6]/g, "ae") - .replace(/[\xE7]/g, "c") - .replace(/[\xE8-\xEB]/g, "e") - .replace(/[\xEC-\xEF]/g, "i") - .replace(/[\xF1]/g, "n") - .replace(/[\xF2-\xF6\xF8]/g, "o") - .replace(/[\xF9-\xFC]/g, "u") - .replace(/[\xFE]/g, "p") - .replace(/[\xFD\xFF]/g, "y"); - } - - return str; -}; - -/** - * Remove non-word chars. - */ -export const removeNonWord = str => { - return str.replace(/[^0-9a-zA-Z\xC0-\xFF \-]/g, ""); -}; - -/** - * Convert string to camelCase text. - */ -export const camelCase = str => { - str = replaceAccents(str); - str = removeNonWord(str) - .replace(/\-/g, " ") // convert all hyphens to spaces - .replace(/\s[a-z]/g, upperCase) // convert first char of each word to UPPERCASE - .replace(/\s+/g, "") // remove spaces - .replace(/^[A-Z]/g, lowerCase); // convert first char to lowercase - return str; -}; - -/** - * Add space between camelCase text. - */ -export const unCamelCase = str => { - str = str.replace(/([a-z\xE0-\xFF])([A-Z\xC0\xDF])/g, "$1 $2"); - str = str.toLowerCase(); // add space between camelCase text - return str; -}; - -/** - * UPPERCASE first char of each word. - */ -export const properCase = str => { - return lowerCase(str).replace(/^\w|\s\w/g, upperCase); -}; - -/** - * camelCase + UPPERCASE first char - */ -export const pascalCase = str => { - return camelCase(str).replace(/^[a-z]/, upperCase); -}; - -/** - * UPPERCASE first char of each sentence and lowercase other chars. - */ -export const sentenceCase = str => { - // Replace first char of each sentence (new line or after '.\s+') to - // UPPERCASE - return lowerCase(str).replace(/(^\w)|\.\s+(\w)/gm, upperCase); -}; - -/** - * Convert to lower case, remove accents, remove non-word chars and - * replace spaces with the specified delimeter. - * Does not split camelCase text. - */ -export const slugify = (str, delimeter) => { - if (delimeter == null) { - delimeter = "-"; - } - - str = replaceAccents(str); - str = removeNonWord(str); - str = trim(str) // should come after removeNonWord - .replace(/ +/g, delimeter) // replace spaces with delimeter - .toLowerCase(); - - return str; -}; - -/** - * Replaces spaces with hyphens, split camelCase text, remove non-word chars, remove accents and convert to lower case. - */ -export const hyphenate = str => { - str = unCamelCase(str); - return slugify(str, "-"); -}; - -/** - * Replaces hyphens with spaces. (only hyphens between word chars) - */ -export const unhyphenate = str => { - return str.replace(/(\w)(-)(\w)/g, "$1 $3"); -}; - -/** - * Replaces spaces with underscores, split camelCase text, remove - * non-word chars, remove accents and convert to lower case. - */ -export const underscore = str => { - str = unCamelCase(str); - return slugify(str, "_"); -}; - -/** - * Convert line-breaks from DOS/MAC to a single standard (UNIX by default) - */ -export const normalizeLineBreaks = (str, lineEnd) => { - lineEnd = lineEnd || "\n"; - - return str - .replace(/\r\n/g, lineEnd) // DOS - .replace(/\r/g, lineEnd) // Mac - .replace(/\n/g, lineEnd); // Unix -}; - -/** - * Searches for a given substring - */ -export const contains = (str, substring, fromIndex) => { - return str.indexOf(substring, fromIndex) !== -1; -}; - -/** - * Truncate string at full words. - */ -export const crop = (str, maxChars, append) => { - return truncate(str, maxChars, append, true); -}; - -/** - * Escape RegExp string chars. - */ -export const escapeRegExp = str => { - const ESCAPE_CHARS = /[\\.+*?\^$\[\](){}\/'#]/g; - return str.replace(ESCAPE_CHARS, "\\$&"); -}; - -/** - * Escapes a string for insertion into HTML. - */ -export const escapeHtml = str => { - str = str - .replace(/&/g, "&") - .replace(//g, ">") - .replace(/'/g, "'") - .replace(/"/g, """); - - return str; -}; - -/** - * Unescapes HTML special chars - */ -export const unescapeHtml = str => { - str = str - .replace(/&/g, "&") - .replace(/</g, "<") - .replace(/>/g, ">") - .replace(/'/g, "'") - .replace(/"/g, '"'); - return str; -}; - -/** - * Escape string into unicode sequences - */ -export const escapeUnicode = (str, shouldEscapePrintable) => { - return str.replace(/[\s\S]/g, function(ch) { - // skip printable ASCII chars if we should not escape them - if (!shouldEscapePrintable && /[\x20-\x7E]/.test(ch)) { - return ch; - } - // we use "000" and slice(-4) for brevity, need to pad zeros, - // unicode escape always have 4 chars after "\u" - return `\\u${`000${ch.charCodeAt(0).toString(16)}`.slice(-4)}`; - }); -}; - -/** - * Remove HTML tags from string. - */ -export const stripHtmlTags = str => { - return str.replace(/<[^>]*>/g, ""); -}; - -export const createSharableLink = solution => { - return `${window.location.protocol}//${window.location.host}${ - window.location.pathname - }?import=${encodeURIComponent(jsonpack.pack(solution))}`; -}; - -/** - * Remove non-printable ASCII chars - */ -export const removeNonASCII = str => { - // Matches non-printable ASCII chars - - // http://en.wikipedia.org/wiki/ASCII#ASCII_printable_characters - return str.replace(/[^\x20-\x7E]/g, ""); -}; - -/** - * Repeat string n times - */ -export const repeat = (str, n) => { - return new Array(n + 1).join(str); -}; - -/** - * Pad string with `char` if its' length is smaller than `minLen` - */ -export const rpad = (str, minLen, ch) => { - ch = ch || " "; - return str.length < minLen ? str + repeat(ch, minLen - str.length) : str; -}; - -/** - * Pad string with `char` if its' length is smaller than `minLen` - */ -export const lpad = (str, minLen, ch) => { - ch = ch || " "; - - return str.length < minLen ? repeat(ch, minLen - str.length) + str : str; -}; - -/** - * Capture all capital letters following a word boundary (in case the - * input is in all caps) - */ -export const abbreviate = str => { - return str.match(/\b([A-Z])/g).join(""); -}; - -export const includeFile = file => { - const script = document.createElement("script"); - script.src = file; - script.type = "text/javascript"; - script.defer = true; - document.head.appendChild(script); -}; - -export const isUndefined = e => typeof e === "undefined"; -const isconst = e => typeof e === "function"; -// eslint-disable-next-line no-restricted-globals -const isNumber = e => typeof e === "number" && isFinite(e); -const isObject = e => typeof e === "object"; -const isArray = e => Array.isArray(e); -const isImage = e => e instanceof HTMLImageElement; -const isNull = e => e === null; -const isInt = e => Number(e) === e && e % 1 === 0; -const isFloat = e => Number(e) === e && e % 1 !== 0; - -export const createSlug = text => { - return text - .toString() - .toLowerCase() - .replace(/\s+/g, "-") // Replace spaces with - - .replace(/[^\w\-]+/g, "") // Remove all non-word chars - .replace(/\-\-+/g, "-") // Replace multiple - with single - - .replace(/^-+/, "") // Trim - from start of text - .replace(/-+$/, ""); // Trim - from end of text -}; - -export const loadScript = src => { - return new Promise((resolve, reject) => { - const script = document.createElement("script"); - - script.onload = resolve; - script.onerror = reject; - - script.src = src; - document.body.appendChild(script); - }); -}; - -// ES6, native Promises, arrow functions, default arguments -// wait(1000).then(() => { -// console.log("b"); -// }); -export const wait = (ms = 0) => { - return new Promise(r => setTimeout(r, ms)); -}; - -export const sleep = (ms = 0) => { - return new Promise(r => setTimeout(r, ms)); -}; - -export const queryParamStringAsObject = fullQueryString => { - let queryString = {}; - const query = fullQueryString; - const vars = query.split("&"); - for (let i = 0; i < vars.length; i++) { - const pair = vars[i].split("="); - if (typeof queryString[pair[0]] === "undefined") { - queryString[pair[0]] = decodeURIComponent(pair[1]); - } else if (typeof queryString[pair[0]] === "string") { - const arr = [queryString[pair[0]], decodeURIComponent(pair[1])]; - queryString[pair[0]] = arr; - } else { - queryString[pair[0]].push(decodeURIComponent(pair[1])); - } - } - queryString = Object.entries(queryString).reduce( - (a, [k, v]) => (k === "" || v == null || v === "undefined" ? a : { ...a, [k]: v }), - {} - ); // Filter null and undefined values - return queryString; -}; - -export const convertTeneoJsonNewToOld = newJson => { - const finalJson = { - responseData: { - status: 0, - isNewSession: false, - lastinput: "", - answer: "", - extraData: {}, - emotion: "", - link: { - href: "", - target: "" - } - } - }; - finalJson.responseData.status = newJson.status; - finalJson.responseData.lastinput = newJson.input.text; - finalJson.responseData.answer = newJson.output.text; - finalJson.responseData.emotion = newJson.output.emotion; - finalJson.responseData.extraData = newJson.output.parameters; - finalJson.responseData.link.href = newJson.output.link; - - return finalJson; -}; - -export const queryParametersAsObject = () => { - const queryString = {}; - const query = window.location.search.substring(1); - const vars = query.split("&"); - for (let i = 0; i < vars.length; i++) { - const pair = vars[i].split("="); - if (typeof queryString[pair[0]] === "undefined") { - queryString[pair[0]] = decodeURIComponent(pair[1]); - } else if (typeof queryString[pair[0]] === "string") { - const arr = [queryString[pair[0]], decodeURIComponent(pair[1])]; - queryString[pair[0]] = arr; - } else { - queryString[pair[0]].push(decodeURIComponent(pair[1])); - } - } - return queryString; -}; - -export const setFullscreen = fullscreen => { - const el = document.documentElement; - if (fullscreen) { - const rfs = - el.requestFullscreen || - el.webkitRequestFullScreen || - el.mozRequestFullScreen || - el.msRequestFullscreen; - rfs.call(el); - } else { - const rfs = - document.exitFullscreen || - document.webkitExitFullscreen || - document.mozExitFullscreen || - document.msExitFullscreen; - rfs.call(document); - } -}; - -export const doesParameterExist = paramName => { - const queryString = window.location.search; - const params = queryString.substring(1).split("&"); - for (let i = 0; i < params.length; i++) { - const pair = params[i].split("="); - if (decodeURIComponent(pair[0]) === paramName) return true; + if (foundSolution) { + return true; } return false; }; -export const download = (data, filename, type = "text/plain") => { - const file = new Blob([data], { - type - }); - if (window.navigator.msSaveOrOpenBlob) - // IE10+ - window.navigator.msSaveOrOpenBlob(file, filename); - else { - // Others - const a = document.createElement("a"); - const url = URL.createObjectURL(file); - a.href = url; - a.download = filename; - document.body.appendChild(a); - a.click(); - setTimeout(function() { - document.body.removeChild(a); - window.URL.revokeObjectURL(url); - }, 0); - } -}; - -export const getBase64Image = img => { - const canvas = document.createElement("canvas"); - canvas.width = img.width; - canvas.height = img.height; - const ctx = canvas.getContext("2d"); - ctx.drawImage(img, 0, 0); - const dataURL = canvas.toDataURL("image/png"); - return dataURL.replace(/^data:image\/(png|jpg);base64,/, ""); -}; - -export const generateRandomId = () => { - return Math.random() - .toString(36) - .replace(/[^a-z]+/g, "") - .substr(2, 10); -}; - -export const cloneObject = obj => { - // this is a deep clone - return obj ? JSON.parse(JSON.stringify(obj)) : obj; -}; - -export const cloneObjectPromise = obj => { - return new Promise((resolve, reject) => { - // this is a deep clone - try { - const clonedObject = obj ? JSON.parse(JSON.stringify(obj)) : obj; - resolve(clonedObject); - } catch (e) { - reject(e); - } - }); -}; - -export const uuidPromise = () => { - return new Promise(resolve => resolve(uuidv4())); -}; - -export const decodeHTML = html => { - const txt = document.createElement("textarea"); - txt.innerHTML = html; - return txt.value; -}; - -export const generateQueryParams = jsObject => - Object.keys(jsObject) - .map(key => `${key}=${jsObject[key]}`) - .join("&"); - -export const getParameterByName = (name, url) => { - if (!url) url = window.location.href; - name = name.replace(/[\]]/g, "\\$&"); - const regex = new RegExp(`[?&]${name}(=([^&#]*)|&|#|$)`); - const results = regex.exec(url); - if (!results) return null; - if (!results[2]) return ""; - return decodeURIComponent(results[2].replace(/\+/g, " ")); -}; - -export const getUrlVarsAsObj = () => { - const vars = {}; - window.location.href.replace(/[?&]+([^=&]+)=([^&]*)/gi, function(m, key, value) { - vars[key] = value; - }); - return vars; -}; - -export const getUrlParam = (parameter, defaultvalue) => { - let urlparameter = ""; - if (window.location.href.indexOf(parameter) > -1) { - urlparameter = this.getUrlVars()[parameter]; - if (urlparameter) { - urlparameter = urlparameter.split("#")[0]; - urlparameter = - urlparameter === "true" ? true : urlparameter === "false" ? false : urlparameter; - } else { - urlparameter = defaultvalue; - } - } else { - urlparameter = defaultvalue; - } - return urlparameter; -}; +export const fixSolution = solution => { + if (!("id" in solution)) { + const id = uuid(); + // TODO: I know I need to fix this... + // if (solution.name === allSolutions.activeSolution) { + // allSolutions.activeSolution = id; + // } + solution.id = id; + } + if (!("responseDelay" in solution)) { + solution.responseDelay = 0; + } + if (!("font" in solution)) { + solution.font = solutionDefault.font; + } + if (!("lookAndFeel" in solution)) { + solution.lookAndFeel = solutionDefault.lookAndFeel; + } + + if (!("custom1" in solution.theme)) { + solution.theme.dark = solutionDefault.theme.dark; + solution.theme.custom1 = solutionDefault.theme.custom1; + solution.theme.custom2 = solutionDefault.theme.custom2; + solution.theme.custom3 = solutionDefault.theme.custom3; + } + + if (!("focusButton" in solution.theme)) { + solution.theme.focusButton = solutionDefault.theme.focusButton; + } + + if (!("sendButton" in solution.theme)) { + solution.theme.sendButton = solution.theme.primary; + } + + if (!("textButton" in solution.theme)) { + solution.theme.textButton = solutionDefault.theme.textButton; + } + + if (!("animations" in solution)) { + solution.animations = solutionDefault.animations; + } + if (!("promptTriggers" in solution)) { + solution.promptTriggers = solutionDefault.promptTriggers; + } + return solution; +}; + +const WHITE_SPACES = [ + " ", + "\n", + "\r", + "\t", + "\f", + "\v", + "\u00A0", + "\u1680", + "\u180E", + "\u2000", + "\u2001", + "\u2002", + "\u2003", + "\u2004", + "\u2005", + "\u2006", + "\u2007", + "\u2008", + "\u2009", + "\u200A", + "\u2028", + "\u2029", + "\u202F", + "\u205F", + "\u3000" +]; + +export const fixSolutions = allSolutions => { + let origChatConfig = JSON.stringify(allSolutions); + origChatConfig = replaceString(origChatConfig, '"true"', "true"); + origChatConfig = replaceString(origChatConfig, '"false"', "false"); + allSolutions = JSON.parse(origChatConfig); + + if ("solutions" in allSolutions) { + allSolutions.solutions.forEach(solution => { + solution = fixSolution(solution); + }); + } else if ("url" in allSolutions) { + // not really a solutions file rather just a solution + const solutionsWrapper = { + activeSolution: "", + solutions: [] + }; + const fixedSolution = fixSolution(allSolutions); + solutionsWrapper.activeSolution = fixedSolution.id; + solutionsWrapper.solutions.push(fixedSolution); + allSolutions = solutionsWrapper; + } + + return allSolutions; +}; + +export const parseExtraData = input => { + let result = null; + if (input) { + try { + result = decodeURIComponent(input); + } catch (e) { + result = input; + } + try { + result = JSON.parse(result); + } catch (e) { + result = null; + } + } + return result; +}; + +/** + * Remove chars from beginning of string. + */ +export const ltrim = (str, chars) => { + chars = chars || WHITE_SPACES; + + let start = 0; + const len = str.length; + const charLen = chars.length; + let found = true; + let i; + let c; + + while (found && start < len) { + found = false; + i = -1; + c = str.charAt(start); + + while (++i < charLen) { + if (c === chars[i]) { + found = true; + start++; + break; + } + } + } + + return start >= len ? "" : str.substr(start, len); +}; + +/** + * Remove chars from end of string. + */ +export const rtrim = (str, chars) => { + chars = chars || WHITE_SPACES; + + let end = str.length - 1; + const charLen = chars.length; + let found = true; + let i; + let c; + + while (found && end >= 0) { + found = false; + i = -1; + c = str.charAt(end); + + while (++i < charLen) { + if (c === chars[i]) { + found = true; + end--; + break; + } + } + } + + return end >= 0 ? str.substring(0, end + 1) : ""; +}; + +/** + * Remove white-spaces from beginning and end of string. + */ +export const trim = (str, chars) => { + chars = chars || WHITE_SPACES; + return ltrim(rtrim(str, chars), chars); +}; + +/** + * Limit number of chars. + */ +export const truncate = (str, maxChars, append, onlyFullWords) => { + append = append || "..."; + maxChars = onlyFullWords ? maxChars + 1 : maxChars; + + str = trim(str); + if (str.length <= maxChars) { + return str; + } + str = str.substr(0, maxChars - append.length); + // crop at last space or remove trailing whitespace + str = onlyFullWords ? str.substr(0, str.lastIndexOf(" ")) : trim(str); + return str + append; +}; + +export const lightOrDark = color => { + // Variables for red, green, blue values + let r; + let g; + let b; + + // Check the format of the color, HEX or RGB? + if (color.match(/^rgb/)) { + // If HEX --> store the red, green, blue values in separate variables + color = color.match(/^rgba?\((\d+),\s*(\d+),\s*(\d+)(?:,\s*(\d+(?:\.\d+)?))?\)$/); + + r = color[1]; + g = color[2]; + b = color[3]; + } else { + // If RGB --> Convert it to HEX: http://gist.github.com/983661 + color = +`0x${color.slice(1).replace(color.length < 5 && /./g, "$&$&")}`; + + r = color >> 16; + g = (color >> 8) & 255; + b = color & 255; + } + + // HSP (Highly Sensitive Poo) equation from http://alienryderflex.com/hsp.html + const hsp = Math.sqrt(0.299 * (r * r) + 0.587 * (g * g) + 0.114 * (b * b)); + + // Using the HSP value, determine whether the color is light or dark 127.5 orig + if (hsp > 145) { + // console.log("Light >> HSP: ", hsp); + return "light"; + } + // console.log("Dark >> HSP: ", hsp); + return "dark"; +}; + +export const isEmpty = obj => { + for (const key in obj) { + return false; + } + return true; +}; + +export const isLight = color => { + if (lightOrDark(color) === "light") { + return true; + } + return false; +}; + +export const isDark = color => { + if (lightOrDark(color) === "dark") { + return true; + } + return false; +}; + +export const sendMessageToParent = message => { + if (window.parent) { + window.parent.postMessage(message, "*"); // post multiple times to each domain you want leopard on. Specifiy origin for each post. + } +}; + +export const replaceAll = (targetStr, findStr, replaceStr = "") => { + return targetStr.split(findStr).join(replaceStr); +}; + +export const removeAll = (targetStr, findArr) => { + findArr.forEach(find => { + targetStr = replaceAll(targetStr, find); + }); + return targetStr; +}; + +export function debounce(func, wait, immediate) { + let timeout; + return function debounceLogic(...args) { + const context = this; + clearTimeout(timeout); + timeout = setTimeout(function callFunction() { + timeout = null; + if (!immediate) func.apply(context, args); + }, wait); + if (immediate && !timeout) func.apply(context, args); + }; +} + +/** + * Smooth scroll + */ +// easing functions http://goo.gl/5HLl8 +Math.easeInOutQuad = function(t, b, c, d) { + t /= d / 2; + if (t < 1) { + return (c / 2) * t * t + b; + } + t--; + return (-c / 2) * (t * (t - 2) - 1) + b; +}; + +Math.easeInCubic = function(t, b, c, d) { + const tc = (t /= d) * t * t; + return b + c * tc; +}; + +Math.inOutQuintic = function(t, b, c, d) { + const ts = (t /= d) * t; + const tc = ts * t; + return b + c * (6 * tc * ts + -15 * ts * ts + 10 * tc); +}; + +// requestAnimationFrame for Smart Animating http://goo.gl/sx5sts +export const requestAnimFrame = (function() { + return ( + window.requestAnimationFrame || + window.webkitRequestAnimationFrame || + window.mozRequestAnimationFrame || + function(callback) { + window.setTimeout(callback, 1000 / 60); + } + ); +})(); + +export const scrollTo = (to, callback, duration) => { + // because it's so fucking difficult to detect the scrolling element, just move them all + function move(amount) { + document.documentElement.scrollTop = amount; + document.body.parentNode.scrollTop = amount; + document.body.scrollTop = amount; + } + function position() { + return ( + document.documentElement.scrollTop || + document.body.parentNode.scrollTop || + document.body.scrollTop + ); + } + const start = position(); + const change = to - start; + let currentTime = 0; + const increment = 20; + duration = typeof duration === "undefined" ? 500 : duration; + const animateScroll = function() { + // increment the time + currentTime += increment; + // find the value with the quadratic in-out easing function + const val = Math.easeInOutQuad(currentTime, start, change, duration); + // move the document.body + move(val); + // do the animation unless its over + if (currentTime < duration) { + requestAnimFrame(animateScroll); + } else if (callback && typeof callback === "function") { + // the animation is done so lets callback + callback(); + } + }; + animateScroll(); +}; +// end smooth scroll + +export const cleanEmptyChunks = answerText => { + let finalAnswerText = ""; + const chunks = answerText.split("||"); + chunks.forEach(chunk => { + const trimmedChunk = chunk.trim(); + if (trimmedChunk) { + finalAnswerText += `||${trimmedChunk}`; + } + }); + if (finalAnswerText.startsWith("||")) { + finalAnswerText = finalAnswerText.substring(2); + } + return finalAnswerText.trim(); +}; + +export const lowerCase = str => { + return str.toLowerCase(); +}; + +/** + * "Safer" String.toUpperCase() + */ +export const upperCase = str => { + return str.toUpperCase(); +}; + +/** + * Replaces all accented chars with regular ones + */ +export const replaceAccents = str => { + // verifies if the String has accents and replace them + if (str.search(/[\xC0-\xFF]/g) > -1) { + str = str + .replace(/[\xC0-\xC5]/g, "A") + .replace(/[\xC6]/g, "AE") + .replace(/[\xC7]/g, "C") + .replace(/[\xC8-\xCB]/g, "E") + .replace(/[\xCC-\xCF]/g, "I") + .replace(/[\xD0]/g, "D") + .replace(/[\xD1]/g, "N") + .replace(/[\xD2-\xD6\xD8]/g, "O") + .replace(/[\xD9-\xDC]/g, "U") + .replace(/[\xDD]/g, "Y") + .replace(/[\xDE]/g, "P") + .replace(/[\xE0-\xE5]/g, "a") + .replace(/[\xE6]/g, "ae") + .replace(/[\xE7]/g, "c") + .replace(/[\xE8-\xEB]/g, "e") + .replace(/[\xEC-\xEF]/g, "i") + .replace(/[\xF1]/g, "n") + .replace(/[\xF2-\xF6\xF8]/g, "o") + .replace(/[\xF9-\xFC]/g, "u") + .replace(/[\xFE]/g, "p") + .replace(/[\xFD\xFF]/g, "y"); + } + + return str; +}; + +/** + * Remove non-word chars. + */ +export const removeNonWord = str => { + return str.replace(/[^0-9a-zA-Z\xC0-\xFF \-]/g, ""); +}; + +/** + * Convert string to camelCase text. + */ +export const camelCase = str => { + str = replaceAccents(str); + str = removeNonWord(str) + .replace(/\-/g, " ") // convert all hyphens to spaces + .replace(/\s[a-z]/g, upperCase) // convert first char of each word to UPPERCASE + .replace(/\s+/g, "") // remove spaces + .replace(/^[A-Z]/g, lowerCase); // convert first char to lowercase + return str; +}; + +/** + * Add space between camelCase text. + */ +export const unCamelCase = str => { + str = str.replace(/([a-z\xE0-\xFF])([A-Z\xC0\xDF])/g, "$1 $2"); + str = str.toLowerCase(); // add space between camelCase text + return str; +}; + +/** + * UPPERCASE first char of each word. + */ +export const properCase = str => { + return lowerCase(str).replace(/^\w|\s\w/g, upperCase); +}; + +/** + * camelCase + UPPERCASE first char + */ +export const pascalCase = str => { + return camelCase(str).replace(/^[a-z]/, upperCase); +}; + +/** + * UPPERCASE first char of each sentence and lowercase other chars. + */ +export const sentenceCase = str => { + // Replace first char of each sentence (new line or after '.\s+') to + // UPPERCASE + return lowerCase(str).replace(/(^\w)|\.\s+(\w)/gm, upperCase); +}; + +/** + * Convert to lower case, remove accents, remove non-word chars and + * replace spaces with the specified delimeter. + * Does not split camelCase text. + */ +export const slugify = (str, delimeter) => { + if (delimeter == null) { + delimeter = "-"; + } + + str = replaceAccents(str); + str = removeNonWord(str); + str = trim(str) // should come after removeNonWord + .replace(/ +/g, delimeter) // replace spaces with delimeter + .toLowerCase(); + + return str; +}; + +/** + * Replaces spaces with hyphens, split camelCase text, remove non-word chars, remove accents and convert to lower case. + */ +export const hyphenate = str => { + str = unCamelCase(str); + return slugify(str, "-"); +}; + +/** + * Replaces hyphens with spaces. (only hyphens between word chars) + */ +export const unhyphenate = str => { + return str.replace(/(\w)(-)(\w)/g, "$1 $3"); +}; + +/** + * Replaces spaces with underscores, split camelCase text, remove + * non-word chars, remove accents and convert to lower case. + */ +export const underscore = str => { + str = unCamelCase(str); + return slugify(str, "_"); +}; + +/** + * Convert line-breaks from DOS/MAC to a single standard (UNIX by default) + */ +export const normalizeLineBreaks = (str, lineEnd) => { + lineEnd = lineEnd || "\n"; + + return str + .replace(/\r\n/g, lineEnd) // DOS + .replace(/\r/g, lineEnd) // Mac + .replace(/\n/g, lineEnd); // Unix +}; + +/** + * Searches for a given substring + */ +export const contains = (str, substring, fromIndex) => { + return str.indexOf(substring, fromIndex) !== -1; +}; + +/** + * Truncate string at full words. + */ +export const crop = (str, maxChars, append) => { + return truncate(str, maxChars, append, true); +}; + +/** + * Escape RegExp string chars. + */ +export const escapeRegExp = str => { + const ESCAPE_CHARS = /[\\.+*?\^$\[\](){}\/'#]/g; + return str.replace(ESCAPE_CHARS, "\\$&"); +}; + +/** + * Escapes a string for insertion into HTML. + */ +export const escapeHtml = str => { + str = str + .replace(/&/g, "&") + .replace(//g, ">") + .replace(/'/g, "'") + .replace(/"/g, """); + + return str; +}; + +/** + * Unescapes HTML special chars + */ +export const unescapeHtml = str => { + str = str + .replace(/&/g, "&") + .replace(/</g, "<") + .replace(/>/g, ">") + .replace(/'/g, "'") + .replace(/"/g, '"'); + return str; +}; + +/** + * Escape string into unicode sequences + */ +export const escapeUnicode = (str, shouldEscapePrintable) => { + return str.replace(/[\s\S]/g, function(ch) { + // skip printable ASCII chars if we should not escape them + if (!shouldEscapePrintable && /[\x20-\x7E]/.test(ch)) { + return ch; + } + // we use "000" and slice(-4) for brevity, need to pad zeros, + // unicode escape always have 4 chars after "\u" + return `\\u${`000${ch.charCodeAt(0).toString(16)}`.slice(-4)}`; + }); +}; + +/** + * Remove HTML tags from string. + */ +export const stripHtmlTags = str => { + return str.replace(/<[^>]*>/g, ""); +}; + +export const createSharableLink = solution => { + return `${window.location.protocol}//${window.location.host}${ + window.location.pathname + }?import=${encodeURIComponent(jsonpack.pack(solution))}`; +}; + +/** + * Remove non-printable ASCII chars + */ +export const removeNonASCII = str => { + // Matches non-printable ASCII chars - + // http://en.wikipedia.org/wiki/ASCII#ASCII_printable_characters + return str.replace(/[^\x20-\x7E]/g, ""); +}; + +/** + * Repeat string n times + */ +export const repeat = (str, n) => { + return new Array(n + 1).join(str); +}; + +/** + * Pad string with `char` if its' length is smaller than `minLen` + */ +export const rpad = (str, minLen, ch) => { + ch = ch || " "; + return str.length < minLen ? str + repeat(ch, minLen - str.length) : str; +}; + +/** + * Pad string with `char` if its' length is smaller than `minLen` + */ +export const lpad = (str, minLen, ch) => { + ch = ch || " "; + + return str.length < minLen ? repeat(ch, minLen - str.length) + str : str; +}; + +/** + * Capture all capital letters following a word boundary (in case the + * input is in all caps) + */ +export const abbreviate = str => { + return str.match(/\b([A-Z])/g).join(""); +}; + +export const includeFile = file => { + const script = document.createElement("script"); + script.src = file; + script.type = "text/javascript"; + script.defer = true; + document.head.appendChild(script); +}; + +export const isUndefined = e => typeof e === "undefined"; +const isconst = e => typeof e === "function"; +// eslint-disable-next-line no-restricted-globals +const isNumber = e => typeof e === "number" && isFinite(e); +const isObject = e => typeof e === "object"; +const isArray = e => Array.isArray(e); +const isImage = e => e instanceof HTMLImageElement; +const isNull = e => e === null; +const isInt = e => Number(e) === e && e % 1 === 0; +const isFloat = e => Number(e) === e && e % 1 !== 0; + +export const createSlug = text => { + return text + .toString() + .toLowerCase() + .replace(/\s+/g, "-") // Replace spaces with - + .replace(/[^\w\-]+/g, "") // Remove all non-word chars + .replace(/\-\-+/g, "-") // Replace multiple - with single - + .replace(/^-+/, "") // Trim - from start of text + .replace(/-+$/, ""); // Trim - from end of text +}; + +export const loadScript = src => { + return new Promise((resolve, reject) => { + const script = document.createElement("script"); + + script.onload = resolve; + script.onerror = reject; + + script.src = src; + document.body.appendChild(script); + }); +}; + +// ES6, native Promises, arrow functions, default arguments +// wait(1000).then(() => { +// console.log("b"); +// }); +export const wait = (ms = 0) => { + return new Promise(r => setTimeout(r, ms)); +}; + +export const sleep = (ms = 0) => { + return new Promise(r => setTimeout(r, ms)); +}; + +export const queryParamStringAsObject = fullQueryString => { + let queryString = {}; + const query = fullQueryString; + const vars = query.split("&"); + for (let i = 0; i < vars.length; i++) { + const pair = vars[i].split("="); + if (typeof queryString[pair[0]] === "undefined") { + queryString[pair[0]] = decodeURIComponent(pair[1]); + } else if (typeof queryString[pair[0]] === "string") { + const arr = [queryString[pair[0]], decodeURIComponent(pair[1])]; + queryString[pair[0]] = arr; + } else { + queryString[pair[0]].push(decodeURIComponent(pair[1])); + } + } + queryString = Object.entries(queryString).reduce( + (a, [k, v]) => (k === "" || v == null || v === "undefined" ? a : { ...a, [k]: v }), + {} + ); // Filter null and undefined values + return queryString; +}; + +export const convertTeneoJsonNewToOld = newJson => { + const finalJson = { + responseData: { + status: 0, + isNewSession: false, + lastinput: "", + answer: "", + extraData: {}, + emotion: "", + link: { + href: "", + target: "" + } + } + }; + finalJson.responseData.status = newJson.status; + finalJson.responseData.lastinput = newJson.input.text; + finalJson.responseData.answer = newJson.output.text; + finalJson.responseData.emotion = newJson.output.emotion; + finalJson.responseData.extraData = newJson.output.parameters; + finalJson.responseData.link.href = newJson.output.link; + + return finalJson; +}; + +export const queryParametersAsObject = () => { + const queryString = {}; + const query = window.location.search.substring(1); + const vars = query.split("&"); + for (let i = 0; i < vars.length; i++) { + const pair = vars[i].split("="); + if (typeof queryString[pair[0]] === "undefined") { + queryString[pair[0]] = decodeURIComponent(pair[1]); + } else if (typeof queryString[pair[0]] === "string") { + const arr = [queryString[pair[0]], decodeURIComponent(pair[1])]; + queryString[pair[0]] = arr; + } else { + queryString[pair[0]].push(decodeURIComponent(pair[1])); + } + } + return queryString; +}; + +export const setFullscreen = fullscreen => { + const el = document.documentElement; + if (fullscreen) { + const rfs = + el.requestFullscreen || + el.webkitRequestFullScreen || + el.mozRequestFullScreen || + el.msRequestFullscreen; + rfs.call(el); + } else { + const rfs = + document.exitFullscreen || + document.webkitExitFullscreen || + document.mozExitFullscreen || + document.msExitFullscreen; + rfs.call(document); + } +}; + +export const doesParameterExist = paramName => { + const queryString = window.location.search; + const params = queryString.substring(1).split("&"); + for (let i = 0; i < params.length; i++) { + const pair = params[i].split("="); + if (decodeURIComponent(pair[0]) === paramName) return true; + } + return false; +}; + +export const download = (data, filename, type = "text/plain") => { + const file = new Blob([data], { + type + }); + if (window.navigator.msSaveOrOpenBlob) + // IE10+ + window.navigator.msSaveOrOpenBlob(file, filename); + else { + // Others + const a = document.createElement("a"); + const url = URL.createObjectURL(file); + a.href = url; + a.download = filename; + document.body.appendChild(a); + a.click(); + setTimeout(function() { + document.body.removeChild(a); + window.URL.revokeObjectURL(url); + }, 0); + } +}; + +export const getBase64Image = img => { + const canvas = document.createElement("canvas"); + canvas.width = img.width; + canvas.height = img.height; + const ctx = canvas.getContext("2d"); + ctx.drawImage(img, 0, 0); + const dataURL = canvas.toDataURL("image/png"); + return dataURL.replace(/^data:image\/(png|jpg);base64,/, ""); +}; + +export const generateRandomId = () => { + return Math.random() + .toString(36) + .replace(/[^a-z]+/g, "") + .substr(2, 10); +}; + +export const cloneObject = obj => { + // this is a deep clone + return obj ? JSON.parse(JSON.stringify(obj)) : obj; +}; + +export const cloneObjectPromise = obj => { + return new Promise((resolve, reject) => { + // this is a deep clone + try { + const clonedObject = obj ? JSON.parse(JSON.stringify(obj)) : obj; + resolve(clonedObject); + } catch (e) { + reject(e); + } + }); +}; + +export const uuidPromise = () => { + return new Promise(resolve => resolve(uuidv4())); +}; + +export const decodeHTML = html => { + const txt = document.createElement("textarea"); + txt.innerHTML = html; + return txt.value; +}; + +export const generateQueryParams = jsObject => + Object.keys(jsObject) + .map(key => `${key}=${jsObject[key]}`) + .join("&"); + +export const getParameterByName = (name, url) => { + if (!url) url = window.location.href; + name = name.replace(/[\]]/g, "\\$&"); + const regex = new RegExp(`[?&]${name}(=([^&#]*)|&|#|$)`); + const results = regex.exec(url); + if (!results) return null; + if (!results[2]) return ""; + return decodeURIComponent(results[2].replace(/\+/g, " ")); +}; + +export const makeSolutionUnique = solution => { + let uniqueSol = solution; + let randomId = generateRandomId(); + uniqueSol.id = uuid(); + uniqueSol.deepLink = slugify(`${solution.deepLink}-${randomId}`); + uniqueSol.name = `${solution.name} - ${randomId}`; + return uniqueSol; +}; + +export const getUrlVarsAsObj = () => { + const vars = {}; + window.location.href.replace(/[?&]+([^=&]+)=([^&]*)/gi, function(m, key, value) { + vars[key] = value; + }); + return vars; +}; + +export const getUrlParam = (parameter, defaultvalue) => { + let urlparameter = ""; + if (window.location.href.indexOf(parameter) > -1) { + urlparameter = this.getUrlVars()[parameter]; + if (urlparameter) { + urlparameter = urlparameter.split("#")[0]; + urlparameter = + urlparameter === "true" ? true : urlparameter === "false" ? false : urlparameter; + } else { + urlparameter = defaultvalue; + } + } else { + urlparameter = defaultvalue; + } + return urlparameter; +}; diff --git a/src/views/Config.vue b/src/views/Config.vue index 351a4b7b..639d5358 100644 --- a/src/views/Config.vue +++ b/src/views/Config.vue @@ -1,1435 +1,1438 @@ - - - - - - - + if (this.$store.getters.activeSolution) { + const activeSolutionPast = this.$store.getters.activeSolution; + const activeSolutionCurrent = this.config.solutions.find( + solution => solution.id === activeSolutionPast.id + ); + if (!skipRefreshDialog && activeSolutionCurrent.id !== this.selectedSolution.id) { + // another solution is selected than what was originally used to enter the config area + this.showPossibleRefreshDialog = true; + return; + } + + if (JSON.stringify(activeSolutionPast) !== JSON.stringify(activeSolutionCurrent)) { + this.refreshBrowserToSolution(activeSolutionCurrent); + return; + } else { + this.showModal = false; + setTimeout(() => { + this.$router.push("/"); + }, 300); + } + } else { + this.showModal = false; + setTimeout(() => { + this.$router.push("/"); + }, 300); + } + }, + refreshBrowserToSolution(solution) { + this.showPossibleRefreshDialog = false; + this.refresh = true; + sessionStorage.removeItem("teneo-chat-history"); // new config delete chat history + let addtionalParams = ""; + if (doesParameterExist("plugin_id")) { + const params = new URLSearchParams(window.location.search); + const pluginId = params.get("plugin_id"); + addtionalParams += `&plugin_id=${pluginId}`; + } + if (doesParameterExist("embed")) { + addtionalParams += "&embed"; + } + if (doesParameterExist("button")) { + addtionalParams += "&button"; + } + window.location = `${location.protocol}//${location.host}${location.pathname}?dl=${solution.deepLink}${addtionalParams}`; + }, + toggleFullscreen() { + let modalElements = document.getElementsByClassName("leopard-config-modal"); + modalElements[0].setAttribute("style", ""); + this.fullscreen = !this.fullscreen; + }, + createShareLinkForSolution() { + copy(createSharableLink(this.selectedSolution)); + this.displaySnackBar("📋 Copied Solution Sharable Import Link to Clipboard 🔗"); + this.snackbarClipboard = true; + }, + closeAddNewSolutionDialog(result) { + logger.info("Supposed to close Add Edit Dialog"); + this.displayAddEditDialog = false; + + if (result) { + this.config = result.config; + this.selectedSolution = this.config.solutions.find( + solution => solution.id === result.selectedSolutionId + ); + } + this.currentModeEdit = ""; + this.saveToLocalStorage(); + const self = this; + setTimeout(function() { + self.solution = cloneObject(SOLUTION_DEFAULT); + self.solution.id = uuid(); + }, 1000); + }, + toggleDisplayOfSolutionConfig() { + this.displayFullSolutionConfig = !this.displayFullSolutionConfig; + }, + importSolution(newSolution) { + let existingSolutionsWithId = this.config.solutions.findIndex( + solution => solution.id === newSolution.id + ); + + let existingSolutionsWithName = this.config.solutions.findIndex( + solution => solution.name === newSolution.name + ); + let existingSolutionsWithDeepLink = this.config.solutions.findIndex( + solution => solution.deepLink === newSolution.deepLink + ); + + if ( + existingSolutionsWithId < 0 && + existingSolutionsWithName < 0 && + existingSolutionsWithDeepLink < 0 + ) { + // no clashes in id, name, deep link + this.config.solutions.push(newSolution); // no conflicts + } else if ( + existingSolutionsWithId >= 0 && + existingSolutionsWithName >= 0 && + existingSolutionsWithDeepLink >= 0 + ) { + // id, name and deep link clash + newSolution.name = newSolution.name + " [imported]"; + newSolution.deepLink = newSolution.deepLink + "-" + generateRandomId(); + newSolution.id = uuid(); + this.config.solutions.push(newSolution); + } else if ( + existingSolutionsWithId < 0 && + existingSolutionsWithName >= 0 && + existingSolutionsWithDeepLink >= 0 + ) { + // name and deep link clash + newSolution.name = newSolution.name + " [imported]"; + newSolution.deepLink = newSolution.deepLink + "-" + generateRandomId(); + this.config.solutions.push(newSolution); + } else if ( + existingSolutionsWithId < 0 && + existingSolutionsWithName >= 0 && + existingSolutionsWithDeepLink < 0 + ) { + // name clash only + newSolution.name = newSolution.name + " [imported]"; + this.config.solutions.push(newSolution); + } else if ( + existingSolutionsWithId >= 0 && + existingSolutionsWithDeepLink < 0 && + existingSolutionsWithName < 0 + ) { + // id clash only + newSolution.id = uuid(); + this.config.solutions.push(newSolution); + } else if ( + existingSolutionsWithId < 0 && + existingSolutionsWithDeepLink >= 0 && + existingSolutionsWithName < 0 + ) { + // deeplink clash only + newSolution.deepLink = newSolution.deepLink + "-" + generateRandomId(); + this.config.solutions.push(newSolution); + } + }, + toggleLoading() { + this.showProgressUpload = true; + }, + readConfigFile(event) { + this.showProgressUpload = true; + var file = event.target.files[0]; + this.uploadConfig = ""; + + let reader = new FileReader(); + let self = this; + reader.onload = function() { + self.uploadConfig = reader.result; + self.showProgressUpload = false; + }; + reader.readAsText(file); + }, + compareSolutions(a, b) { + if (a.name < b.name) return -1; + if (a.name > b.name) return 1; + return 0; + }, + downloadSolutionConfig() { + download( + this.getFullSolutionConfig, + `leopard-all-config-${dayjs().format("YYYYMMDD[-]H[-]mm")}.txt` + ); + + let now = dayjs(); + localStorage.setItem(STORAGE_KEY + "lastBackupDate", now.format()); + }, + downloadSelectedSolutionConfig() { + download( + JSON.stringify(this.selectedSolution, null, 2), + `leopard-${this.selectedSolution.name + .replace(/[|&;$%@"<>()+,]/g, "") + .replace(/\s+/g, "-") + .toLowerCase()}-config-${dayjs().format("YYYYMMDD[-]H[-]mm")}.txt` + ); + }, + copyWholeConfigClipboard() { + copy(JSON.stringify(this.config, null, 2)); + this.displaySnackBar("📋 Copied All Solution Configs to Clipboard"); + this.snackbarClipboard = true; + }, + copySolutionToClipboard() { + copy(JSON.stringify(this.selectedSolution, null, 2)); + this.displaySnackBar("📋 Copied Solution Config to Clipboard"); + }, + setActiveSolutionAsSelected() { + // pre select the solution active in the browser + if (this.$store.getters.activeSolution) { + this.selectedSolution = this.$store.getters.activeSolution; + } else { + // fallback to the default active solutions + this.selectedSolution = this.config.solutions.find( + solution => solution.id === this.config.activeSolution + ); + } + }, + setSolutionAsSelected(solutionId) { + this.selectedSolution = this.config.solutions.find(solution => solution.id === solutionId); + }, + refreshBrowser() { + if (this.selectedSolution) { + this.refresh = true; + sessionStorage.removeItem("teneo-chat-history"); // new config delete chat history + let addtionalParams = ""; + if (doesParameterExist("plugin_id")) { + const params = new URLSearchParams(window.location.search); + const pluginId = params.get("plugin_id"); + addtionalParams += `&plugin_id=${pluginId}`; + } + if (doesParameterExist("embed")) { + addtionalParams += "&embed"; + } + if (doesParameterExist("button")) { + addtionalParams += "&button"; + } + window.location = `${location.protocol}//${location.host}${location.pathname}?dl=${this.selectedSolution.deepLink}${addtionalParams}`; + } else { + window.location = `${location.protocol}//${location.host}${location.pathname}`; + } + }, + saveToLocalStorage() { + this.$store.commit("SET_CHAT_CONFIG", this.config); + localStorage.setItem(STORAGE_KEY + "config", JSON.stringify(this.config)); + logger.debug(`Saved all solutions to localStorage`); + }, + editSolution() { + if (this.selectedSolution !== null) { + this.dialogTitle = "Editing Solution"; + this.currentModeEdit = "edit"; + this.solution = cloneObject(this.selectedSolution); // make a copy - we have a save button + logger.info("Trying to open Add Edit Dialod"); + this.showAddEditDialog(); + } + }, + setActiveSolution() { + this.config.activeSolution = this.selectedSolution.id; + this.saveToLocalStorage(); + }, + cloneSolution() { + const newName = this.selectedSolution.name + " - Copy"; + let clonedSolution = cloneObject(this.selectedSolution); + clonedSolution.id = uuid(); + clonedSolution.name = newName; + const duplicateSolutions = this.config.solutions.filter( + solution => solution.name === newName + ); + if (duplicateSolutions.length > 0) { + clonedSolution.name = clonedSolution.name + " [" + generateRandomId() + "]"; + } + clonedSolution.deepLink = clonedSolution.deepLink + "-" + generateRandomId(); + this.config.solutions.push(clonedSolution); + this.selectedSolution = cloneObject(clonedSolution); + this.displaySnackBar("Solution was cloned. New name is " + clonedSolution.name, 3000); + this.saveToLocalStorage(); + }, + displayUploadSnackBar(message, timeout = 2000, color = "#2F2869") { + this.uploadSnackbar = false; + this.globalSnackbarMessage = message; + this.uploadSnackbar = true; + this.globalSnackbarTimeout = timeout; + this.globalSnackbarColor = color; + }, + displaySnackBar(message, timeout = 2000, color = "#2F2869") { + this.globalSnackbar = false; + this.globalSnackbarMessage = message; + this.globalSnackbar = true; + this.globalSnackbarTimeout = timeout; + this.globalSnackbarColor = color; + }, + deleteSolution(solutionId) { + this.config.solutions = this.config.solutions.filter(solution => { + return solution.id !== solutionId; + }); + + this.audit.results = this.audit.results.filter(result => { + return result.solution.id !== solutionId; + }); + + this.saveToLocalStorage(); + }, + editSolutionAudit(solutionId) { + let foundSolution = this.config.solutions.find(solution => solution.id === solutionId); + this.selectedSolution = foundSolution; + this.solution = cloneObject(this.selectedSolution); // make a copy - we have a save button + this.dialogTitle = `Editing Solution | ${this.selectedSolution.name}`; + this.currentModeEdit = "edit"; + + this.showAddEditDialog(); + }, + deleteSolutionConfig() { + if (this.selectedSolution) { + const theId = this.selectedSolution.id; + if (this.config.activeSolution === theId) { + this.config.activeSolution = ""; + } + this.config.solutions = this.config.solutions.filter( + obj => JSON.stringify(obj) !== JSON.stringify(this.selectedSolution) + ); + if (this.config.solutions.length === 1) { + this.config.activeSolution = this.config.solutions[0].id; + this.selectedSolution = cloneObject(this.config.solutions[0]); + } else if (this.config.solutions.length > 1) { + let self = this; + this.selectedSolution = cloneObject( + this.config.solutions.find(function(solution) { + return solution.id === self.config.activeSolution; + }) + ); + } else { + this.selectedSolution = null; + } + this.displaySnackBar("Solution was deleted", 3000); + this.ensureDefaultSolutionIsSet(); + this.saveToLocalStorage(); + this.setActiveSolutionAsSelected(); + } + }, + ensureDefaultSolutionIsSet() { + // this.config.activeSolution + let foundActiveSolution = this.config.solutions.find( + solution => solution.id === this.config.activeSolution + ); + if (!foundActiveSolution && this.config.solutions.length > 0) { + this.config.activeSolution = this.config.solutions[0].id; + } + }, + closeUploadDialog() { + this.newConfig = ""; + this.uploadConfig = ""; + this.uploadDialog = false; + }, + saveUploadForm() { + // let inputValue = this.$refs.newConfig.inputValue; + if (this.uploadConfig && this.uploadConfig.trim()) { + let newConfig = ""; + try { + newConfig = JSON.parse(this.uploadConfig); + } catch (error) { + this.displaySnackBar("Please provide a valid configuration", 3000); + return; + } + if (newConfig && "activeSolution" in newConfig) { + // ok uploading a full config + if ("activeSolution" in this.config) { + // let's merge + newConfig.solutions.forEach(newSolution => { + this.importSolution(newSolution); + }); + this.displaySnackBar("Merged existing full config with newly uploded", 3000); + } else { + // current config is empty + this.config = fixSolutions(newConfig); + this.displaySnackBar("Imported a new full configuration", 3000); + } + this.setActiveSolutionAsSelected(); + } else if (newConfig && "name" in newConfig) { + // uploading a single config - add it to the current solution config + newConfig = fixSolution(newConfig); + this.importSolution(newConfig); // import individual solution + this.setSolutionAsSelected(newConfig.id); + this.displaySnackBar("Imported as " + newConfig.name, 3000); + } + + this.config = fixSolutions(this.config); + + this.closeUploadDialog(); + this.saveToLocalStorage(); + } else { + this.displayUploadSnackBar("Please provide a valid configuration", 3000, "red"); + } + }, + addSolution() { + this.dialogTitle = "Creating a new solution configuration"; + this.currentModeEdit = ""; // meaning add + this.solution = cloneObject(SOLUTION_DEFAULT); + this.solution.id = uuid(); + this.showAddEditDialog(); + }, + showAddEditDialog() { + this.displayAddEditDialog = true; + logger.info(`Ok add edit dialog should be showing...`); + }, + showUploadDialog() { + this.uploadDialog = true; + } + } +}; + + + + +