Skip to content

Commit

Permalink
Merge pull request #44 from roundtableAI/v012-2
Browse files Browse the repository at this point in the history
  • Loading branch information
timshell authored Aug 22, 2024
2 parents e4d4a3e + 7782189 commit f9ca612
Show file tree
Hide file tree
Showing 20 changed files with 1,765 additions and 1,477 deletions.
2 changes: 1 addition & 1 deletion examples/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
</div>
<div id = 'finish'></div>
</div>
<script type="module" src="studies/enterprise-market-research.js"></script>
<script type="module" src="studies/element-showcase.js"></script>
</body>

</html>
147 changes: 147 additions & 0 deletions examples/studies/element-showcase.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
import Survey from '../../library/core/survey.js';
import BoundingBox from '../../library/elements/boundingBox.js';
import CheckBox from '../../library/elements/checkBox.js';
import DropdownSelect from '../../library/elements/dropdownSelect.js';
import Grid from '../../library/elements/grid.js';
import HTML from '../../library/elements/HTML.js';
import MultiSelect from '../../library/elements/multiSelect.js';
import NumberEntry from '../../library/elements/numberEntry.js';
import NumberScale from '../../library/elements/numberScale.js';
import OpenEnd from '../../library/elements/openEnd.js';
import SingleSelect from '../../library/elements/singleSelect.js';
import ProgressBar from '../../library/plugins/progressBar.js';
import pageHTML from '../../library/plugins/pageHTML.js';

async function runComprehensiveSurvey() {
const survey = new Survey({
title: "Comprehensive Survey Example",
description: "This survey demonstrates all available question types.",
styles: {
body: {
background: '#f9f9f7',
}
}
});

const htmlIntro = new HTML({
id: 'intro',
content: '<h1>Welcome to our element showcase</h1><p>This survey include examples of every question type and plugin. The code for the survey is available on our <a href="https://github.com/roundtableAI/roundtable-js" target="_blank">GitHub</a>.</p>',
styles: {
root: {
textAlign: 'center',
}
}
});

const singleSelect = new SingleSelect({
id: 'favorite-color',
text: 'What is your favorite color?',
subText: 'Select one option',
options: ['Red', 'Blue', 'Green', 'Yellow'],
required: true,
allowOther: true,
});

const multiSelect = new MultiSelect({
id: 'hobbies',
text: 'Which of the following are your hobbies? (Select all that apply)',
options: ['Reading', 'Sports', 'Cooking', 'Gaming', 'Traveling'],
required: true,
minSelect: 1,
maxSelect: 3
});

const dropdownSelect = new DropdownSelect({
id: 'country',
text: 'Select your country of residence',
options: ['United States', 'Canada', 'United Kingdom', 'Australia', 'Germany', 'France', 'Japan', 'Other'],
required: true
});

const checkBox = new CheckBox({
id: 'terms',
text: 'I agree to the terms and conditions',
required: true
});

const numberEntry = new NumberEntry({
id: 'age',
text: 'Please enter your age',
min: 18,
max: 100,
required: true
});

const numberScale = new NumberScale({
id: 'satisfaction',
text: 'How satisfied are you with our service?',
min: 1,
max: 10,
minLabel: 'Not at all satisfied',
maxLabel: 'Extremely satisfied',
required: true
});

const openEnd = new OpenEnd({
id: 'feedback',
text: 'Please provide any additional feedback you may have',
required: false,
maxLength: 500
});

const grid = new Grid({
id: 'feature-rating',
text: 'Please rate the following features of our product',
rows: ['Ease of use', 'Performance', 'Design', 'Customer support'],
columns: ['Poor', 'Fair', 'Good', 'Excellent'],
required: true
});

const boundingBox = new BoundingBox({
id: 'image-selection',
text: 'Please draw a box around the house',
imageUrl: 'https://images.pexels.com/photos/106399/pexels-photo-106399.jpeg',
required: true
});

// Group questions into pages
const page1 = { id: 'page1', elements: [htmlIntro, singleSelect, multiSelect, dropdownSelect] };
const page2 = { id: 'page2', elements: [checkBox, numberEntry, numberScale, openEnd] };
const page3 = { id: 'page3', elements: [grid, boundingBox] };

const progress = new ProgressBar({ maxPages: 3 });

const logo = new pageHTML({
id: 'logo',
content: '<img src="https://roundtable.ai/images/logo-with-text.svg" alt="Company Logo">',
position: 'top',
styles:{
root: {
margin: '0 auto',
width: '100%',
maxWidth: '180px',
marginTop: '5px',
marginBottom: '10px'
}
}
});
survey.addPlugin(logo);
survey.addPlugin(progress);

// Show pages sequentially
await survey.showPage(page1);
await survey.showPage(page2);
await survey.showPage(page3);

// Finish the survey
survey.finishSurvey(`
<h2>Thank you for completing our comprehensive survey!</h2>
<p>Your responses have been recorded and will help us improve our services.</p>
`);

// Log the survey data (in a real scenario, you'd probably send this to a server)
console.log('Survey data:', survey.getAllSurveyData());
}

// Run the survey
runComprehensiveSurvey();
20 changes: 11 additions & 9 deletions examples/studies/enterprise-market-research.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import OpenEnd from '../../library/elements/openEnd.js';
import HTML from '../../library/elements/HTML.js';
import MultiSelect from '../../library/elements/multiSelect.js';
import SingleSelect from '../../library/elements/singleSelect.js';
import OrderedScale from '../../library/elements/orderedScale.js';
import NumberScale from '../../library/elements/NumberScale.js';
import Grid from '../../library/elements/grid.js';
import NumberEntry from '../../library/elements/numberEntry.js';
import ProgressBar from '../../library/plugins/progressBar.js';
Expand All @@ -29,13 +29,15 @@ async function runSurvey() {
backgroundColor: '#5f9ea0',
}
},
question: {
borderBottom: '1px solid #b0d4ff',
paddingBottom: '20px',
'@media (max-width: 650px)': {
borderBottom: 'none',
paddingBottom: '0px',
},
Element: {
root:{
borderBottom: '1px solid #b0d4ff',
paddingBottom: '20px',
'@media (max-width: 650px)': {
borderBottom: 'none',
paddingBottom: '0px',
},
}
}
}
});
Expand Down Expand Up @@ -93,7 +95,7 @@ async function runSurvey() {
await survey.showPage({ id: `page_${destination}`, elements: [q_destination] });
}

const q3 = new OrderedScale({
const q3 = new NumberScale({
id: 'travel_importance',
text: 'How important are the following factors when choosing a business travel destination?',
min: 1,
Expand Down
126 changes: 74 additions & 52 deletions library/core/element.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,18 @@ class Element {
responseTimestamp: null
};

static defaultStyles = {};
static styleKeys = ['root', 'innerContainer', 'textContainer', 'text', 'subText', 'errorMessage'];

static selectorMap = {
root: '',
innerContainer: '.inner-container',
textContainer: '.text-container',
text: '.question-text',
subText: '.question-subtext',
errorMessage: '.error-message'
};

constructor({ id, type, store_data = false, required = false }) {
constructor({ id, type, store_data = false, required = false, customValidation = null, styles = {} }) {
if (!id || typeof id !== 'string') {
throw new Error('Invalid id: must be a non-empty string');
}
Expand All @@ -21,45 +30,36 @@ class Element {
this.required = Boolean(required);
this.data = { id, type, response: null, responded: false };
this.initialResponse = null;
this.styles = {};
this.styles = styles;
this.eventListeners = [];
this.elementStyleKeys = [...this.constructor.styleKeys];
this.selectorMap = { ...this.constructor.selectorMap };
this.customValidation = customValidation;
}

mergeStyles(surveyElementStyles, elementStyles) {
const mergedStyles = {};
this.elementStyleKeys.forEach(key => {
mergedStyles[key] = {
...(surveyElementStyles[key] || {}),
...(this.constructor.defaultStyles?.[key] || {}),
...(this.styles[key] || {}),
...(elementStyles[key] || {})
};
});
return mergedStyles;
}

validate() {
if (this.required && !this.data.response) {
this.showValidationError('This question is required. Please provide an answer.');
return false;
generateStylesheet(surveyElementStyles) {
const mergedStyles = this.mergeStyles(surveyElementStyles, this.styles);
return this.elementStyleKeys.map(key => {
return this.generateStyleForSelector(this.getSelectorForKey(key), mergedStyles[key])
}
this.showValidationError(null);
return true;
}

showValidationError(message) {
const errorElement = document.getElementById(`${this.id}-error`);
if (errorElement) {
errorElement.textContent = message || '';
errorElement.style.display = message ? 'block' : 'none';
} else {
console.warn(`Error element not found for ${this.id}`);
}
}

mergeStyles(defaultStyles, customStyles) {
this.styles = this.constructor.styleKeys.reduce((merged, key) => {
merged[key] = { ...defaultStyles[key], ...customStyles[key] };
return merged;
}, {});
}

generateStylesheet() {
return this.constructor.styleKeys.map(key =>
this.generateStyleForSelector(this.getSelectorForKey(key), this.styles[key])
).join('\n');
}

getSelectorForKey(key) {
// This method should be overridden by subclasses
return '';
return this.selectorMap[key] || '';
}

generateStyleForSelector(selector, rules) {
Expand All @@ -68,7 +68,8 @@ class Element {
return '';
}

const fullSelector = selector ? `#${this.id}-container ${selector}` : `#${this.id}-container`;
const baseSelector = `#${this.id}-container`;
const fullSelector = selector ? `${baseSelector} ${selector}` : baseSelector;
const baseStyles = this.rulesToString(rules);
let styleString = `${fullSelector} { ${baseStyles} }`;

Expand Down Expand Up @@ -100,26 +101,25 @@ class Element {
this.initialResponse = value;
}

render() {
render(surveyElementStyles) {
const questionContainer = document.getElementById('question-container');
if (questionContainer) {
const elementContainer = document.createElement('div');
elementContainer.id = `${this.id}-container`;
elementContainer.innerHTML = this.generateHTML();
const elementHtml = this.generateHTML();
const tempContainer = document.createElement('div');
tempContainer.innerHTML = elementHtml;

const elementContainer = tempContainer.firstElementChild;

// Apply styles
const styleElement = document.createElement('style');
styleElement.textContent = this.generateStylesheet();
styleElement.textContent = this.generateStylesheet(surveyElementStyles);
elementContainer.prepend(styleElement);

questionContainer.appendChild(elementContainer);
this.attachEventListeners();

// Set the initial response after rendering
if (this.initialResponse !== null) {
this.setResponse(this.initialResponse);
this.initialResponse = null;
}
this.data.response = this.initialResponse;
} else {
console.error('Question container not found');
}
Expand Down Expand Up @@ -154,25 +154,47 @@ class Element {
this.showValidationError(null);
}

setResponded() {
addData(key, value) {
if (this.store_data) {
this.data.responded = true;
this.data[key] = value;
}
}

addData(key, value) {
if (this.store_data) {
this.data[key] = value;
validate() {
let isValid = true;
let errorMessage = '';

// Check if the question is required and answered
if (this.required && !this.data.responded) {
isValid = false;
errorMessage = 'Please provide a response.';
}

// If basic validation passed and custom validation is provided, run it
if (isValid && typeof this.customValidation === 'function') {
const customValidationResult = this.customValidation(this.data.response);
if (customValidationResult !== true) {
isValid = false;
errorMessage = customValidationResult || 'Invalid input.';
}
}

return { isValid, errorMessage };
}

isValid() {
return this.validate();
showValidationError(message) {
const errorElement = document.getElementById(`${this.id}-error`);
if (errorElement) {
errorElement.textContent = message || '';
errorElement.style.display = message ? 'block' : 'none';
} else {
console.warn(`Error element not found for ${this.id}`);
}
}

destroy() {
// Remove all event listeners if they exist
if (this.eventListeners){
if (this.eventListeners) {
this.eventListeners.forEach(({ element, eventType, handler }) => {
element.removeEventListener(eventType, handler);
});
Expand Down
Loading

0 comments on commit f9ca612

Please sign in to comment.