From ec7d1f6a3da977465c35c57166c885e5e5ef8123 Mon Sep 17 00:00:00 2001 From: Li Yi Yu Date: Mon, 2 Oct 2023 16:21:01 -0400 Subject: [PATCH 1/2] update surveys with popup changes and multiple questions support --- src/extensions/surveys.ts | 967 +++++++++++++++++++++++------------ src/posthog-surveys-types.ts | 4 + 2 files changed, 648 insertions(+), 323 deletions(-) diff --git a/src/extensions/surveys.ts b/src/extensions/surveys.ts index 9d1ae3661..471be00eb 100644 --- a/src/extensions/surveys.ts +++ b/src/extensions/surveys.ts @@ -6,10 +6,9 @@ import { RatingSurveyQuestion, Survey, SurveyAppearance, + SurveyQuestion, } from '../posthog-surveys-types' -const posthogLogo = - '' const satisfiedEmoji = '' const neutralEmoji = @@ -21,223 +20,294 @@ const veryDissatisfiedEmoji = const verySatisfiedEmoji = '' const cancelSVG = - '' + '' +const posthogLogo = + '' +const checkSVG = + '' -const style = (id: string, appearance: SurveyAppearance | null) => ` - .survey-${id}-form { - position: fixed; - bottom: 3vh; - right: 20px; - color: black; - font-weight: normal; - font-family: -apple-system, BlinkMacSystemFont, "Inter", "Segoe UI", "Roboto", Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"; - text-align: left; - max-width: ${parseInt(appearance?.maxWidth || '320')}px; - z-index: ${parseInt(appearance?.zIndex || '99999')}; - } - .form-submit[disabled] { - opacity: 0.6; - filter: grayscale(100%); - cursor: not-allowed; - } - .survey-${id}-form { - flex-direction: column; - background: ${appearance?.backgroundColor || 'white'}; - border: 1px solid #f0f0f0; - border-radius: 8px; - padding-top: 5px; - box-shadow: -6px 0 16px -8px rgb(0 0 0 / 8%), -9px 0 28px 0 rgb(0 0 0 / 5%), -12px 0 48px 16px rgb(0 0 0 / 3%); - } - .survey-${id}-form textarea { - color: #2d2d2d; - font-size: 14px; - font-family: -apple-system, BlinkMacSystemFont, "Inter", "Segoe UI", "Roboto", Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"; - background: white; - color: black; - outline: none; - padding-left: 10px; - padding-right: 10px; - padding-top: 10px; - border-radius: 6px; - margin: 0.5rem; - } - .form-submit { - box-sizing: border-box; - margin: 0; - font-family: inherit; - overflow: visible; - text-transform: none; - line-height: 1.5715; - position: relative; - display: inline-block; - font-weight: 400; - white-space: nowrap; - text-align: center; - border: 1px solid transparent; - cursor: pointer; - transition: all 0.3s cubic-bezier(0.645, 0.045, 0.355, 1); - user-select: none; - touch-action: manipulation; - height: 32px; - padding: 4px 15px; - font-size: 14px; - border-radius: 4px; - outline: 0; - background: ${appearance?.submitButtonColor || '#2C2C2C'} !important; - color: #E5E7E0; - text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.12); - box-shadow: 0 2px 0 rgba(0, 0, 0, 0.045); - } - .form-submit:hover { - filter: brightness(1.2); - } - .form-cancel { - float: right; - border: none; - background: ${appearance?.backgroundColor || 'white'}; - cursor: pointer; - } - .bolded { font-weight: 600; } - .bottom-section { - padding-bottom: .5rem; - } - .buttons { - display: flex; - justify-content: center; - } - .footer-branding { - color: #6a6b69; - font-size: 10.5px; - padding-top: .5rem; - text-align: center; - } - .survey-${id}-box { - padding: .5rem 1rem; - display: flex; - flex-direction: column; - } - .survey-question { - padding-top: 4px; - padding-bottom: 4px; - font-weight: 500; - color: ${appearance?.textColor || 'black'}; - } - .question-textarea-wrapper { - display: flex; - flex-direction: column; - padding-bottom: 4px; - } - .description { - font-size: 14px; - margin-top: .75rem; - margin-bottom: .75rem; - color: ${appearance?.descriptionTextColor || '#4b4b52'}; - } - .ratings-number { - background-color: ${appearance?.ratingButtonColor || '#e0e2e8'}; - font-size: 14px; - border-radius: 6px; - border: 1px solid ${appearance?.ratingButtonColor || '#e0e2e8'}; - padding: 8px; - } - .ratings-number:hover { - cursor: pointer; - filter: brightness(1.1); - } - .rating-options { - margin-top: .5rem; - } - .rating-options-buttons { - display: flex; - justify-content: space-evenly; - } - .max-numbers { - min-width: 280px; - } - .rating-options-emoji { - display: flex; - justify-content: space-evenly; - } - .ratings-emoji { - font-size: 16px; - background-color: transparent; - border: none; - } - .ratings-emoji:hover { - cursor: pointer; - } - .emoji-svg { - fill: ${appearance?.ratingButtonColor || 'black'}; - } - .emoji-svg:hover { - fill: ${appearance?.ratingButtonHoverColor || 'coral'}; - } - .rating-text { - display: flex; - flex-direction: row; - font-size: 12px; - justify-content: space-between; - margin-top: .5rem; - margin-bottom: .5rem; - color: #4b4b52; - } - .rating-section { - margin-bottom: .5rem; - } - .multiple-choice-options { - margin-bottom: .5rem; - margin-top: .5rem; - font-size: 14px; - } - .multiple-choice-options .choice-option { - display: flex; - align-items: center; - gap: 4px; - background: #00000003; - font-size: 14px; - padding: 10px 20px 10px 15px; - border: 1px solid #0000000d; - border-radius: 4px; - cursor: pointer; - margin-bottom: 6px; - } - .multiple-choice-options .choice-option:hover { - background: #0000000a; - } - .multiple-choice-options input { - cursor: pointer; - } - .multiple-choice-options label { - width: 100%; - cursor: pointer; - } - .thank-you-message { - position: fixed; - bottom: 8vh; - right: 20px; - border-radius: 8px; - z-index: ${parseInt(appearance?.zIndex || '99999')}; - box-shadow: -6px 0 16px -8px rgb(0 0 0 / 8%), -9px 0 28px 0 rgb(0 0 0 / 5%), -12px 0 48px 16px rgb(0 0 0 / 3%); - font-family: -apple-system, BlinkMacSystemFont, "Inter", "Segoe UI", "Roboto", Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"; - } - .thank-you-message-container { - background: ${appearance?.backgroundColor || 'white'}; - border: 1px solid #f0f0f0; - border-radius: 8px; - padding: 12px 18px; - text-align: center; - max-width: 320px; - min-width: 150px; - } - .thank-you-message { - color: ${appearance?.textColor || 'black'}; - } - .thank-you-message-body { - padding-bottom: 8px; - font-size: 14px; - color: ${appearance?.descriptionTextColor || '#4b4b52'}; - } - ` +const style = (id: string, appearance: SurveyAppearance | null) => { + const positions = { + left: 'left: 30px;', + right: 'right: 30px;', + center: ` + left: 50%; + transform: translateX(-50%); + `, + } + return ` + .survey-${id}-form { + position: fixed; + margin: 0px; + bottom: 0px; + color: black; + font-weight: normal; + font-family: -apple-system, BlinkMacSystemFont, "Inter", "Segoe UI", "Roboto", Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"; + text-align: left; + max-width: ${parseInt(appearance?.maxWidth || '290')}px; + z-index: ${parseInt(appearance?.zIndex || '99999')}; + border: 1.5px solid ${appearance?.borderColor || '#c9c6c6'}; + border-bottom: 0px; + width: 100%; + ${positions[appearance?.position || 'right'] || 'right: 30px;'} + } + .survey-${id}-form .tab { + display: none; + } + .form-submit[disabled] { + opacity: 0.6; + filter: grayscale(100%); + cursor: not-allowed; + } + .survey-${id}-form { + flex-direction: column; + background: ${appearance?.backgroundColor || '#eeeded'}; + border-top-left-radius: 10px; + border-top-right-radius: 10px; + box-shadow: -6px 0 16px -8px rgb(0 0 0 / 8%), -9px 0 28px 0 rgb(0 0 0 / 5%), -12px 0 48px 16px rgb(0 0 0 / 3%); + } + .survey-${id}-form textarea { + color: #2d2d2d; + font-size: 14px; + font-family: -apple-system, BlinkMacSystemFont, "Inter", "Segoe UI", "Roboto", Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"; + background: white; + color: black; + outline: none; + padding-left: 10px; + padding-right: 10px; + padding-top: 10px; + border-radius: 6px; + border-color: ${appearance?.borderColor || '#c9c6c6'}; + margin-top: 14px; + } + .form-submit { + box-sizing: border-box; + margin: 0; + font-family: inherit; + overflow: visible; + text-transform: none; + position: relative; + display: inline-block; + font-weight: 700; + white-space: nowrap; + text-align: center; + border: 1.5px solid transparent; + cursor: pointer; + user-select: none; + touch-action: manipulation; + padding: 12px; + font-size: 14px; + border-radius: 6px; + outline: 0; + background: ${appearance?.submitButtonColor || 'black'} !important; + text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.12); + box-shadow: 0 2px 0 rgba(0, 0, 0, 0.045); + width: 100%; + } + .form-cancel { + float: right; + border: none; + background: none; + cursor: pointer; + } + .cancel-btn-wrapper { + position: absolute; + width: 35px; + height: 35px; + border-radius: 100%; + top: 0; + right: 0; + transform: translate(50%, -50%); + background: white; + border: 1.5px solid ${appearance?.borderColor || '#c9c6c6'}; + display: flex; + justify-content: center; + align-items: center; + } + .bolded { font-weight: 600; } + .buttons { + display: flex; + justify-content: center; + } + .footer-branding { + font-size: 11px; + margin-top: 10px; + text-align: center; + display: flex; + justify-content: center; + gap: 4px; + align-items: center; + font-weight: 500; + background: ${appearance?.backgroundColor || '#eeeded'}; + text-decoration: none; + } + .survey-${id}-box { + padding: 20px 25px 10px; + display: flex; + flex-direction: column; + } + .survey-question { + font-weight: 500; + font-size: 14px; + background: ${appearance?.backgroundColor || '#eeeded'}; + } + .question-textarea-wrapper { + display: flex; + flex-direction: column; + } + .description { + font-size: 13px; + margin-top: 5px; + opacity: .60; + background: ${appearance?.backgroundColor || '#eeeded'}; + } + .ratings-number { + background-color: ${appearance?.ratingButtonColor || 'white'}; + font-size: 14px; + padding: 8px 0px; + border: none; + } + .ratings-number:hover { + cursor: pointer; + } + .rating-options { + margin-top: 14px; + } + .rating-options-buttons { + display: grid; + border-radius: 6px; + overflow: hidden; + border: 1.5px solid ${appearance?.borderColor || '#c9c6c6'}; + } + .rating-options-buttons > .ratings-number { + border-right: 1px solid ${appearance?.borderColor || '#c9c6c6'}; + } + .rating-options-buttons > .ratings-number:last-of-type { + border-right: 0px; + } + .rating-options-buttons .rating-active { + background: ${appearance?.ratingButtonActiveColor || 'black'}; + } + .rating-options-emoji { + display: flex; + justify-content: space-between; + } + .ratings-emoji { + font-size: 16px; + background-color: transparent; + border: none; + padding: 0px; + } + .ratings-emoji:hover { + cursor: pointer; + } + .ratings-emoji.rating-active svg { + fill: ${appearance?.ratingButtonActiveColor || 'black'}; + } + .emoji-svg { + fill: ${appearance?.ratingButtonColor || '#c9c6c6'}; + } + .rating-text { + display: flex; + flex-direction: row; + font-size: 11px; + justify-content: space-between; + margin-top: 6px; + background: ${appearance?.backgroundColor || '#eeeded'}; + opacity: .60; + } + .multiple-choice-options { + margin-top: 13px; + font-size: 14px; + } + .multiple-choice-options .choice-option { + display: flex; + align-items: center; + gap: 4px; + font-size: 13px; + cursor: pointer; + margin-bottom: 5px; + position: relative; + } + .multiple-choice-options > .choice-option:last-of-type { + margin-bottom: 0px; + } + + .multiple-choice-options input { + cursor: pointer; + position: absolute; + opacity: 0; + } + .choice-check { + position: absolute; + right: 10px; + background: white; + } + .choice-check svg { + display: none; + } + .multiple-choice-options .choice-option:hover .choice-check svg { + display: inline-block; + opacity: .25; + } + .multiple-choice-options input:checked + label + .choice-check svg { + display: inline-block; + opacity: 100% !important; + } + .multiple-choice-options input[type=checkbox]:checked + label { + font-weight: bold; + } + .multiple-choice-options input:checked + label { + border: 1.5px solid rgba(0,0,0); + } + .multiple-choice-options label { + width: 100%; + cursor: pointer; + padding: 10px; + border: 1.5px solid rgba(0,0,0,.25); + border-radius: 4px; + background: white; + } + .thank-you-message { + position: fixed; + bottom: 0px; + z-index: ${parseInt(appearance?.zIndex || '99999')}; + box-shadow: -6px 0 16px -8px rgb(0 0 0 / 8%), -9px 0 28px 0 rgb(0 0 0 / 5%), -12px 0 48px 16px rgb(0 0 0 / 3%); + font-family: -apple-system, BlinkMacSystemFont, "Inter", "Segoe UI", "Roboto", Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"; + border-top-left-radius: 10px; + border-top-right-radius: 10px; + padding: 20px 25px 10px; + background: ${appearance?.backgroundColor || '#eeeded'}; + border: 1.5px solid ${appearance?.borderColor || '#c9c6c6'}; + text-align: center; + max-width: ${parseInt(appearance?.maxWidth || '290')}px; + min-width: 150px; + width: 100%; + ${positions[appearance?.position || 'right'] || 'right: 30px;'} + } + .thank-you-message { + color: ${appearance?.textColor || 'black'}; + } + .thank-you-message-body { + margin-top: 6px; + font-size: 14px; + color: ${appearance?.descriptionTextColor || '#4b4b52'}; + } + .thank-you-message-header { + margin: 10px 0px 0px; + } + .thank-you-message-container .form-submit { + margin-top: 20px; + margin-bottom: 10px; + } + .thank-you-message-countdown { + margin-left: 6px; + } + .bottom-section { + margin-top: 14px; + } + ` +} export const createShadow = (styleSheet: string, surveyId: string) => { const div = document.createElement('div') @@ -262,8 +332,11 @@ export const closeSurveyPopup = (surveyId: string, surveyPopup: HTMLFormElement) surveyPopup.reset() } -export const createOpenTextPopup = (posthog: PostHog, survey: Survey) => { - const question = survey.questions[0] as BasicSurveyQuestion | LinkSurveyQuestion +export const createOpenTextOrLinkPopup = ( + posthog: PostHog, + survey: Survey, + question: BasicSurveyQuestion | LinkSurveyQuestion +) => { const surveyQuestionType = question.type const surveyDescription = question.description const questionText = question.question @@ -273,39 +346,67 @@ export const createOpenTextPopup = (posthog: PostHog, survey: Survey) => {
-
${questionText}
- ${surveyDescription ? `${surveyDescription}` : ''} - ${surveyQuestionType === 'open' ? `` : ''} +
${questionText}
+ ${surveyDescription ? `${surveyDescription}` : ''} + ${ + surveyQuestionType === 'open' + ? `` + : '' + }
- +
- + Survey by ${posthogLogo}
` - const formElement = Object.assign(document.createElement('form'), { - className: `survey-${survey.id}-form`, - innerHTML: form, - onsubmit: function (e: any) { - e.preventDefault() - const surveyQuestionType = question.type - posthog.capture('survey sent', { - $survey_name: survey.name, - $survey_id: survey.id, - $survey_question: survey.questions[0], - $survey_response: surveyQuestionType === 'open' ? e.target.survey.value : 'link clicked', - sessionRecordingUrl: posthog.get_session_replay_url(), - }) + let formElement: HTMLFormElement | HTMLDivElement + if (survey.questions.length === 1) { + formElement = Object.assign(document.createElement('form'), { + className: `survey-${survey.id}-form`, + innerHTML: form, + onsubmit: function (e: any) { + e.preventDefault() + const surveyQuestionType = question.type + posthog.capture('survey sent', { + $survey_name: survey.name, + $survey_id: survey.id, + $survey_question: survey.questions[0].question, + $survey_response: surveyQuestionType === 'open' ? e.target.survey.value : 'link clicked', + sessionRecordingUrl: posthog.get_session_replay_url?.(), + }) + if (surveyQuestionType === 'link') { + window.open(question.link || undefined) + } + window.setTimeout(() => { + window.dispatchEvent(new Event('PHSurveySent')) + }, 200) + closeSurveyPopup(survey.id, formElement as HTMLFormElement) + }, + }) + } else { + formElement = Object.assign(document.createElement('div'), { + innerHTML: form, + }) + const submitButton = formElement.querySelector('.form-submit') as HTMLButtonElement + submitButton.addEventListener('click', () => { if (surveyQuestionType === 'link') { window.open(question.link || undefined) } - window.setTimeout(() => { - window.dispatchEvent(new Event('PHSurveySent')) - }, 200) - closeSurveyPopup(survey.id, formElement) - }, + }) + } + + formElement.addEventListener('input', (e: any) => { + if (formElement.querySelector('.form-submit')) { + const submitButton = formElement.querySelector('.form-submit') as HTMLButtonElement + submitButton.disabled = !e.data + } }) return formElement @@ -314,12 +415,16 @@ export const createOpenTextPopup = (posthog: PostHog, survey: Survey) => { export const createThankYouMessage = (survey: Survey) => { const thankYouHTML = `
+
+ +

${survey.appearance?.thankYouMessageHeader || 'Thank you!'}

${survey.appearance?.thankYouMessageDescription || ''}
+ ${ survey.appearance?.whiteLabel ? '' - : `` + : `Survey by ${posthogLogo}` }
` @@ -351,16 +456,16 @@ export const addCancelListeners = ( }) } -export const createRatingsPopup = (posthog: PostHog, survey: Survey) => { - const question = survey.questions[0] as RatingSurveyQuestion +export const createRatingsPopup = (posthog: PostHog, survey: Survey, question: RatingSurveyQuestion) => { const scale = question.scale const displayType = question.display const ratingOptionsElement = document.createElement('div') if (displayType === 'number') { - ratingOptionsElement.className = `rating-options-buttons ${scale === 10 ? 'max-numbers' : ''}` + ratingOptionsElement.className = 'rating-options-buttons' + ratingOptionsElement.style.gridTemplateColumns = `repeat(${scale}, minmax(0, 1fr))` for (let i = 1; i <= scale; i++) { const buttonElement = document.createElement('button') - buttonElement.className = `ratings-number rating_${i}` + buttonElement.className = `ratings-number rating_${i} auto-text-color` buttonElement.type = 'submit' buttonElement.value = `${i}` buttonElement.innerHTML = `${i}` @@ -384,96 +489,159 @@ export const createRatingsPopup = (posthog: PostHog, survey: Survey) => {
-
${question.question}
- ${question.description ? `${question.description}` : ''} +
${question.question}
+ ${question.description ? `${question.description}` : ''}
-
+ ${ + question.lowerBoundLabel || question.upperBoundLabel + ? `
${question.lowerBoundLabel || ''}
${question.upperBoundLabel || ''}
+
` + : '' + } +
+
+
- + Survey by ${posthogLogo} +
` - const formElement = Object.assign(document.createElement('form'), { - className: `survey-${survey.id}-form`, - innerHTML: ratingsForm, - }) + let formElement: HTMLFormElement | HTMLDivElement + if (survey.questions.length === 1) { + formElement = Object.assign(document.createElement('form'), { + className: `survey-${survey.id}-form`, + innerHTML: ratingsForm, + onsubmit: (e: Event) => { + e.preventDefault() + const activeRatingEl = formElement.querySelector('.rating-active') + posthog.capture('survey sent', { + $survey_name: survey.name, + $survey_id: survey.id, + $survey_question: question.question, + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore // TODO: Fix this, error because it doesn't know that the target is a button + $survey_response: parseInt(activeRatingEl?.value), + sessionRecordingUrl: posthog.get_session_replay_url?.(), + }) + window.setTimeout(() => { + window.dispatchEvent(new Event('PHSurveySent')) + }, 200) + closeSurveyPopup(survey.id, formElement as HTMLFormElement) + }, + }) + } else { + formElement = Object.assign(document.createElement('div'), { + innerHTML: ratingsForm, + }) + } + formElement.getElementsByClassName('rating-options')[0].insertAdjacentElement('afterbegin', ratingOptionsElement) for (const x of Array(question.scale).keys()) { - formElement.getElementsByClassName(`rating_${x + 1}`)[0].addEventListener('click', (e: Event) => { + const ratingEl = formElement.getElementsByClassName(`rating_${x + 1}`)[0] + ratingEl.addEventListener('click', (e) => { e.preventDefault() - posthog.capture('survey sent', { - $survey_name: survey.name, - $survey_id: survey.id, - $survey_question: question.question, - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore // TODO: Fix this, error because it doesn't know that the target is a button - $survey_response: parseInt(e.currentTarget?.value), - sessionRecordingUrl: posthog.get_session_replay_url(), - }) - window.setTimeout(() => { - window.dispatchEvent(new Event('PHSurveySent')) - }, 200) - closeSurveyPopup(survey.id, formElement) + for (const activeRatingEl of formElement.getElementsByClassName('rating-active')) { + activeRatingEl.classList.remove('rating-active') + } + ratingEl.classList.add('rating-active') + if (formElement.querySelector('.form-submit')) { + ;(formElement.querySelector('.form-submit') as HTMLButtonElement).disabled = false + } + setTextColors(formElement) }) } return formElement } -export const createMultipleChoicePopup = (posthog: PostHog, survey: Survey) => { - const surveyQuestion = survey.questions[0].question - const surveyDescription = survey.questions[0].description - const surveyQuestionChoices = (survey.questions[0] as MultipleSurveyQuestion).choices - const singleOrMultiSelect = survey.questions[0].type +export const createMultipleChoicePopup = (posthog: PostHog, survey: Survey, question: MultipleSurveyQuestion) => { + const surveyQuestion = question.question + const surveyDescription = question.description + const surveyQuestionChoices = question.choices + const singleOrMultiSelect = question.type const form = `
-
${surveyQuestion}
- ${surveyDescription ? `${surveyDescription}` : ''} +
${surveyQuestion}
+ ${surveyDescription ? `${surveyDescription}` : ''}
${surveyQuestionChoices .map((option, idx) => { const inputType = singleOrMultiSelect === 'single_choice' ? 'radio' : 'checkbox' const singleOrMultiSelectString = `
-
` + ${checkSVG}
` return singleOrMultiSelectString }) .join(' ')}
- +
- + Survey by ${posthogLogo}
` - const formElement = Object.assign(document.createElement('form'), { - className: `survey-${survey.id}-form`, - innerHTML: form, - onsubmit: (e: any) => { - e.preventDefault() - const selectedChoices = - singleOrMultiSelect === 'single_choice' - ? e.target.querySelector('input[type=radio]:checked').value - : [...e.target.querySelectorAll('input[type=checkbox]:checked')].map((choice) => choice.value) - posthog.capture('survey sent', { - $survey_name: survey.name, - $survey_id: survey.id, - $survey_question: survey.questions[0], - $survey_response: selectedChoices, - sessionRecordingUrl: posthog.get_session_replay_url(), - }) - closeSurveyPopup(survey.id, formElement) - }, + let formElement: HTMLFormElement | HTMLDivElement + if (survey.questions.length === 1) { + formElement = Object.assign(document.createElement('form'), { + className: `survey-${survey.id}-form`, + innerHTML: form, + onsubmit: (e: Event) => { + e.preventDefault() + const targetElement = e.target as HTMLFormElement + const selectedChoices = + singleOrMultiSelect === 'single_choice' + ? (targetElement.querySelector('input[type=radio]:checked') as HTMLInputElement)?.value + : [ + ...(targetElement.querySelectorAll( + 'input[type=checkbox]:checked' + ) as NodeListOf), + ].map((choice) => choice.value) + posthog.capture('survey sent', { + $survey_name: survey.name, + $survey_id: survey.id, + $survey_question: survey.questions[0].question, + $survey_response: selectedChoices, + sessionRecordingUrl: posthog.get_session_replay_url?.(), + }) + window.setTimeout(() => { + window.dispatchEvent(new Event('PHSurveySent')) + }, 200) + closeSurveyPopup(survey.id, formElement as HTMLFormElement) + }, + }) + } else { + formElement = Object.assign(document.createElement('div'), { + innerHTML: form, + }) + } + formElement.addEventListener('change', () => { + const selectedChoices = + singleOrMultiSelect === 'single_choice' + ? formElement.querySelector('input[type=radio]:checked') + : formElement.querySelectorAll('input[type=checkbox]:checked') + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore // TODO: Fix this, error because it doesn't recognize node list as an array + if (selectedChoices && (selectedChoices.length ?? 0) > 0) { + ;(formElement.querySelector('.form-submit') as HTMLButtonElement).disabled = false + } else { + ;(formElement.querySelector('.form-submit') as HTMLButtonElement).disabled = true + } }) + return formElement } @@ -494,45 +662,63 @@ export const callSurveys = (posthog: PostHog, forceReload: boolean = false) => { } if (!localStorage.getItem(`seenSurvey_${survey.id}`)) { + const shadow = createShadow(style(survey.id, survey?.appearance), survey.id) let surveyPopup - const surveyQuestionType = survey.questions[0].type - if (surveyQuestionType === 'rating') { - surveyPopup = createRatingsPopup(posthog, survey) - } else if (surveyQuestionType === 'open' || surveyQuestionType === 'link') { - surveyPopup = createOpenTextPopup(posthog, survey) - } else if (surveyQuestionType === 'single_choice' || surveyQuestionType === 'multiple_choice') { - surveyPopup = createMultipleChoicePopup(posthog, survey) + if (survey.questions.length < 2) { + surveyPopup = createSingleQuestionSurvey( + posthog, + survey, + survey.questions[0] + ) as HTMLFormElement + } else { + surveyPopup = createMultipleQuestionSurvey(posthog, survey) } - - if (!surveyPopup) { - console.error(`PostHog: Survey question type: ${surveyQuestionType} not supported`) - return + if (surveyPopup) { + addCancelListeners(posthog, surveyPopup, survey.id, survey.name) + if (survey.appearance?.whiteLabel) { + ;( + surveyPopup.getElementsByClassName('footer-branding')[0] as HTMLAnchorElement + ).style.display = 'none' + } + shadow.appendChild(surveyPopup) } - - const shadow = createShadow(style(survey.id, survey?.appearance), survey.id) - - addCancelListeners(posthog, surveyPopup, survey.id, survey.name) - if (survey.appearance?.whiteLabel) { - ;( - surveyPopup.getElementsByClassName('footer-branding') as HTMLCollectionOf - )[0].style.display = 'none' + if (survey.questions.length > 1) { + const currentQuestion = 0 + showQuestion(currentQuestion, survey.id) } - shadow.appendChild(surveyPopup) - + setTextColors(shadow) window.dispatchEvent(new Event('PHSurveyShown')) posthog.capture('survey shown', { $survey_name: survey.name, $survey_id: survey.id, - sessionRecordingUrl: posthog.get_session_replay_url(), + sessionRecordingUrl: posthog.get_session_replay_url?.(), }) localStorage.setItem(`lastSeenSurveyDate`, new Date().toISOString()) if (survey.appearance?.displayThankYouMessage) { window.addEventListener('PHSurveySent', () => { const thankYouElement = createThankYouMessage(survey) shadow.appendChild(thankYouElement) - window.setTimeout(() => { - thankYouElement.remove() - }, 2000) + const cancelButtons = thankYouElement.querySelectorAll('.form-cancel, .form-submit') + for (const button of cancelButtons) { + button.addEventListener('click', () => { + thankYouElement.remove() + }) + } + const countdownEl = thankYouElement.querySelector('.thank-you-message-countdown') + if (countdownEl) { + let count = 3 + countdownEl.textContent = `(${count})` + const countdown = setInterval(() => { + count -= 1 + if (count <= 0) { + clearInterval(countdown) + thankYouElement.remove() + return + } + countdownEl.textContent = `(${count})` + }, 1000) + } + setTextColors(shadow) }) } } @@ -541,6 +727,141 @@ export const callSurveys = (posthog: PostHog, forceReload: boolean = false) => { }, forceReload) } +export const createMultipleQuestionSurvey = (posthog: PostHog, survey: Survey) => { + const questions = survey.questions + const questionTypeMapping = { + open: createOpenTextOrLinkPopup, + link: createOpenTextOrLinkPopup, + rating: createRatingsPopup, + single_choice: createMultipleChoicePopup, + multiple_choice: createMultipleChoicePopup, + } + const multipleQuestionForm = Object.assign(document.createElement('form'), { + className: `survey-${survey.id}-form`, + onsubmit: (e: Event) => { + e.preventDefault() + const multipleQuestionResponses: Record = {} + const allTabs = (e.target as HTMLDivElement).getElementsByClassName('tab') + let idx = 0 + for (const tab of allTabs) { + const classes = tab.classList + const questionType = classes[2] + let response + if (questionType === 'open') { + response = tab.querySelector('textarea')?.value + } else if (questionType === 'link') { + response = 'link clicked' + } else if (questionType === 'rating') { + response = parseInt((tab.querySelector('.rating-active') as HTMLButtonElement)?.value) + } else if (questionType === 'single_choice' || questionType === 'multiple_choice') { + const selectedChoices = + questionType === 'single_choice' + ? (tab.querySelector('input[type=radio]:checked') as HTMLInputElement).value + : [ + ...(tab.querySelectorAll( + 'input[type=checkbox]:checked' + ) as NodeListOf), + ].map((choice) => choice.value) + response = selectedChoices + } + if (response !== undefined) { + if (idx === 0) { + multipleQuestionResponses['$survey_response'] = response + } else { + multipleQuestionResponses[`$survey_response_${idx}`] = response + } + } + idx++ + } + posthog.capture('survey sent', { + $survey_name: survey.name, + $survey_id: survey.id, + $survey_questions: survey.questions.map((question) => question.question), + sessionRecordingUrl: posthog.get_session_replay_url?.(), + ...multipleQuestionResponses, + }) + window.setTimeout(() => { + window.dispatchEvent(new Event('PHSurveySent')) + }, 200) + closeSurveyPopup(survey.id, multipleQuestionForm) + }, + }) + + questions.map((question, idx) => { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore // TODO: Fix this, error because of survey question type mapping + const questionElement = questionTypeMapping[question.type](posthog, survey, question) + const questionTab = document.createElement('div') + questionTab.className = `tab question-${idx} ${question.type}` + if (idx < questions.length - 1) { + const questionElementSubmitButton = questionElement.getElementsByClassName( + 'form-submit' + )[0] as HTMLButtonElement + questionElementSubmitButton.innerText = 'Next' + questionElementSubmitButton.type = 'button' + questionElementSubmitButton.addEventListener('click', () => { + nextQuestion(idx, survey.id) + }) + } + questionTab.insertAdjacentElement('beforeend', questionElement) + + multipleQuestionForm.insertAdjacentElement('beforeend', questionTab) + }) + + return multipleQuestionForm +} + +const createSingleQuestionSurvey = (posthog: PostHog, survey: Survey, question: SurveyQuestion) => { + const questionType = question.type + if (questionType === 'rating') { + return createRatingsPopup(posthog, survey, question) + } else if (questionType === 'open' || questionType === 'link') { + return createOpenTextOrLinkPopup(posthog, survey, question) + } else if (questionType === 'single_choice' || questionType === 'multiple_choice') { + return createMultipleChoicePopup(posthog, survey, question) + } + return null +} + +function getTextColor(el: HTMLElement) { + const backgroundColor = window.getComputedStyle(el).backgroundColor + if (backgroundColor === 'rgba(0, 0, 0, 0)') { + return 'black' + } + const colorMatch = backgroundColor.match(/^rgba?\((\d+),\s*(\d+),\s*(\d+)(?:,\s*(\d+(?:\.\d+)?))?\)$/) + if (!colorMatch) return 'black' + + const r = parseInt(colorMatch[1]) + const g = parseInt(colorMatch[2]) + const b = parseInt(colorMatch[3]) + const hsp = Math.sqrt(0.299 * (r * r) + 0.587 * (g * g) + 0.114 * (b * b)) + return hsp > 127.5 ? 'black' : 'white' +} + +function setTextColors(parentEl: any) { + for (const el of parentEl.querySelectorAll('.auto-text-color')) { + el.style.color = getTextColor(el) + } +} + +function showQuestion(n: number, surveyId: string) { + // This function will display the specified tab of the form... + const tabs = document + .getElementsByClassName(`PostHogSurvey${surveyId}`)[0] + ?.shadowRoot?.querySelectorAll('.tab') as NodeListOf + tabs[n].style.display = 'block' +} + +function nextQuestion(currentQuestionIdx: number, surveyId: string) { + // figure out which tab to display + const tabs = document + .getElementsByClassName(`PostHogSurvey${surveyId}`)[0] + ?.shadowRoot?.querySelectorAll('.tab') as NodeListOf + + tabs[currentQuestionIdx].style.display = 'none' + showQuestion(currentQuestionIdx + 1, surveyId) +} + export function generateSurveys(posthog: PostHog) { callSurveys(posthog, true) diff --git a/src/posthog-surveys-types.ts b/src/posthog-surveys-types.ts index d50dd262c..f8af82ff1 100644 --- a/src/posthog-surveys-types.ts +++ b/src/posthog-surveys-types.ts @@ -12,11 +12,15 @@ export interface SurveyAppearance { submitButtonText?: string descriptionTextColor?: string ratingButtonColor?: string + ratingButtonActiveColor?: string ratingButtonHoverColor?: string whiteLabel?: boolean displayThankYouMessage?: boolean thankYouMessageHeader?: string thankYouMessageDescription?: string + borderColor?: string + position?: 'left' | 'right' | 'center' + placeholder?: string // questionable: Not in frontend/src/types.ts -> SurveyAppearance, but used in site app maxWidth?: string zIndex?: string From 771b3584d1275deb0fbf40c374cdadf086bc80e2 Mon Sep 17 00:00:00 2001 From: Li Yi Yu Date: Mon, 2 Oct 2023 16:35:08 -0400 Subject: [PATCH 2/2] update test --- src/__tests__/extensions/surveys.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/__tests__/extensions/surveys.js b/src/__tests__/extensions/surveys.js index 3cb97c086..516a744c1 100644 --- a/src/__tests__/extensions/surveys.js +++ b/src/__tests__/extensions/surveys.js @@ -81,9 +81,13 @@ describe('survey display logic', () => { }) // submit the survey - const submitButton = document + const ratingButton = document .getElementsByClassName(`PostHogSurvey${mockSurveys[0].id}`)[0] .shadowRoot.querySelectorAll('.rating_1')[0] + ratingButton.click() + const submitButton = document + .getElementsByClassName(`PostHogSurvey${mockSurveys[0].id}`)[0] + .shadowRoot.querySelectorAll('.form-submit')[0] submitButton.click() expect(mockPostHog.capture).toBeCalledTimes(2) expect(mockPostHog.capture).toBeCalledWith('survey sent', {