rails_onboarding 0.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/MIT-LICENSE +20 -0
- data/README.md +1351 -0
- data/Rakefile +6 -0
- data/app/assets/javascripts/rails_onboarding/admin/chart_controller.js +25 -0
- data/app/assets/javascripts/rails_onboarding/admin/filter_controller.js +49 -0
- data/app/assets/javascripts/rails_onboarding/admin/flash_controller.js +21 -0
- data/app/assets/javascripts/rails_onboarding/admin/flow_editor_controller.js +86 -0
- data/app/assets/javascripts/rails_onboarding/application.js +231 -0
- data/app/assets/javascripts/rails_onboarding/milestone_celebration_controller.js +177 -0
- data/app/assets/javascripts/rails_onboarding/milestone_dashboard_controller.js +132 -0
- data/app/assets/javascripts/rails_onboarding/milestone_detail_controller.js +34 -0
- data/app/assets/javascripts/rails_onboarding/navigation_controller.js +277 -0
- data/app/assets/javascripts/rails_onboarding/onboarding_controller.js +326 -0
- data/app/assets/javascripts/rails_onboarding/progress_controller.js +203 -0
- data/app/assets/javascripts/rails_onboarding/progressive_disclosure_controller.js +218 -0
- data/app/assets/javascripts/rails_onboarding/tooltip_controller.js +1237 -0
- data/app/assets/javascripts/rails_onboarding/tooltip_scheduler_controller.js +511 -0
- data/app/assets/javascripts/rails_onboarding/tour_controller.js +763 -0
- data/app/assets/stylesheets/rails_onboarding/accessibility.css +536 -0
- data/app/assets/stylesheets/rails_onboarding/admin.css +816 -0
- data/app/assets/stylesheets/rails_onboarding/application.css +1145 -0
- data/app/assets/stylesheets/rails_onboarding/flash_messages.css +237 -0
- data/app/assets/stylesheets/rails_onboarding/milestones.css +545 -0
- data/app/assets/stylesheets/rails_onboarding/mobile.css +856 -0
- data/app/assets/stylesheets/rails_onboarding/progressive_disclosure.css +299 -0
- data/app/assets/stylesheets/rails_onboarding/tooltips.css +468 -0
- data/app/assets/stylesheets/rails_onboarding/tour.css +495 -0
- data/app/assets/stylesheets/rails_onboarding/utilities.css +393 -0
- data/app/controllers/concerns/rails_onboarding/admin_authorization.rb +98 -0
- data/app/controllers/concerns/rails_onboarding/rate_limitable.rb +104 -0
- data/app/controllers/rails_onboarding/ab_tests_controller.rb +257 -0
- data/app/controllers/rails_onboarding/admin/ab_tests_controller.rb +121 -0
- data/app/controllers/rails_onboarding/admin/base_controller.rb +68 -0
- data/app/controllers/rails_onboarding/admin/dashboard_controller.rb +189 -0
- data/app/controllers/rails_onboarding/admin/flows_controller.rb +175 -0
- data/app/controllers/rails_onboarding/admin/users_controller.rb +366 -0
- data/app/controllers/rails_onboarding/application_controller.rb +9 -0
- data/app/controllers/rails_onboarding/milestones_controller.rb +126 -0
- data/app/controllers/rails_onboarding/onboarding_controller.rb +409 -0
- data/app/controllers/rails_onboarding/progressive_features_controller.rb +134 -0
- data/app/controllers/rails_onboarding/templates_controller.rb +162 -0
- data/app/controllers/rails_onboarding/test_sessions_controller.rb +16 -0
- data/app/controllers/rails_onboarding/tooltips_controller.rb +92 -0
- data/app/helpers/rails_onboarding/application_helper.rb +4 -0
- data/app/helpers/rails_onboarding/i18n_helper.rb +137 -0
- data/app/helpers/rails_onboarding/personalization_helper.rb +184 -0
- data/app/helpers/rails_onboarding/templates_helper.rb +253 -0
- data/app/jobs/rails_onboarding/application_job.rb +4 -0
- data/app/mailers/rails_onboarding/application_mailer.rb +6 -0
- data/app/mailers/rails_onboarding/onboarding_mailer.rb +72 -0
- data/app/models/concerns/rails_onboarding/ab_testable.rb +149 -0
- data/app/models/concerns/rails_onboarding/onboardable.rb +649 -0
- data/app/models/concerns/rails_onboarding/personalizable.rb +211 -0
- data/app/models/concerns/rails_onboarding/progressive_disclosure.rb +276 -0
- data/app/models/rails_onboarding/analytics.rb +255 -0
- data/app/models/rails_onboarding/analytics_event.rb +168 -0
- data/app/models/rails_onboarding/application_record.rb +5 -0
- data/app/models/rails_onboarding/flow.rb +41 -0
- data/app/services/rails_onboarding/milestone_service.rb +105 -0
- data/app/views/layouts/rails_onboarding/admin.html.erb +106 -0
- data/app/views/layouts/rails_onboarding/application.html.erb +65 -0
- data/app/views/rails_onboarding/ab_tests/index.html.erb +209 -0
- data/app/views/rails_onboarding/ab_tests/results.html.erb +253 -0
- data/app/views/rails_onboarding/admin/ab_tests/_ab_test_card.html.erb +24 -0
- data/app/views/rails_onboarding/admin/ab_tests/index.html.erb +57 -0
- data/app/views/rails_onboarding/admin/ab_tests/show.html.erb +41 -0
- data/app/views/rails_onboarding/admin/dashboard/index.html.erb +224 -0
- data/app/views/rails_onboarding/admin/flows/_form.html.erb +147 -0
- data/app/views/rails_onboarding/admin/flows/edit.html.erb +7 -0
- data/app/views/rails_onboarding/admin/flows/index.html.erb +125 -0
- data/app/views/rails_onboarding/admin/flows/new.html.erb +7 -0
- data/app/views/rails_onboarding/admin/flows/preview.html.erb +40 -0
- data/app/views/rails_onboarding/admin/flows/show.html.erb +114 -0
- data/app/views/rails_onboarding/admin/users/index.html.erb +180 -0
- data/app/views/rails_onboarding/admin/users/show.html.erb +187 -0
- data/app/views/rails_onboarding/milestones/index.html.erb +64 -0
- data/app/views/rails_onboarding/milestones/show.html.erb +57 -0
- data/app/views/rails_onboarding/onboarding/_custom_footer.html.erb +2 -0
- data/app/views/rails_onboarding/onboarding/_progress_indicator.html.erb +56 -0
- data/app/views/rails_onboarding/onboarding/_step_navigation.html.erb +44 -0
- data/app/views/rails_onboarding/onboarding/step.html.erb +38 -0
- data/app/views/rails_onboarding/onboarding/welcome.html.erb +37 -0
- data/app/views/rails_onboarding/onboarding_mailer/completion_email.html.erb +109 -0
- data/app/views/rails_onboarding/onboarding_mailer/completion_email.text.erb +29 -0
- data/app/views/rails_onboarding/onboarding_mailer/reminder_email.html.erb +99 -0
- data/app/views/rails_onboarding/onboarding_mailer/reminder_email.text.erb +20 -0
- data/app/views/rails_onboarding/onboarding_mailer/step_completed_email.html.erb +105 -0
- data/app/views/rails_onboarding/onboarding_mailer/step_completed_email.text.erb +19 -0
- data/app/views/rails_onboarding/onboarding_mailer/welcome_email.html.erb +74 -0
- data/app/views/rails_onboarding/onboarding_mailer/welcome_email.text.erb +17 -0
- data/app/views/rails_onboarding/shared/_flash.html.erb +25 -0
- data/app/views/rails_onboarding/shared/_milestone_badge.html.erb +22 -0
- data/app/views/rails_onboarding/shared/_milestone_celebration.html.erb +44 -0
- data/app/views/rails_onboarding/shared/_onboarding_banner.html.erb +48 -0
- data/app/views/rails_onboarding/templates/index.html.erb +255 -0
- data/config/importmap.rb +33 -0
- data/config/locales/en.yml +62 -0
- data/config/locales/es.yml +62 -0
- data/config/locales/fr.yml +62 -0
- data/config/routes.rb +79 -0
- data/lib/generators/rails_onboarding/install_generator.rb +124 -0
- data/lib/generators/rails_onboarding/templates/README +120 -0
- data/lib/generators/rails_onboarding/templates/add_analytics_to_rails_onboarding.rb +33 -0
- data/lib/generators/rails_onboarding/templates/add_milestone_tracking_to_users.rb +38 -0
- data/lib/generators/rails_onboarding/templates/add_onboarding_indexes.rb +93 -0
- data/lib/generators/rails_onboarding/templates/add_onboarding_to_users.rb +51 -0
- data/lib/generators/rails_onboarding/templates/add_robustness_fields_to_users.rb.tt +20 -0
- data/lib/generators/rails_onboarding/templates/create_rails_onboarding_flows.rb +18 -0
- data/lib/generators/rails_onboarding/templates/onboarding.css +18 -0
- data/lib/generators/rails_onboarding/templates/rails_onboarding.rb +75 -0
- data/lib/generators/rails_onboarding/update_generator.rb +73 -0
- data/lib/rails_onboarding/api_mode.rb +367 -0
- data/lib/rails_onboarding/backfill.rb +130 -0
- data/lib/rails_onboarding/background_jobs.rb +412 -0
- data/lib/rails_onboarding/caching.rb +180 -0
- data/lib/rails_onboarding/cdn_support.rb +202 -0
- data/lib/rails_onboarding/configuration/ab_testing.rb +24 -0
- data/lib/rails_onboarding/configuration/analytics.rb +29 -0
- data/lib/rails_onboarding/configuration/integrations.rb +31 -0
- data/lib/rails_onboarding/configuration/milestones.rb +92 -0
- data/lib/rails_onboarding/configuration/personalization.rb +25 -0
- data/lib/rails_onboarding/configuration/progressive_disclosure.rb +41 -0
- data/lib/rails_onboarding/configuration/rate_limiting.rb +18 -0
- data/lib/rails_onboarding/configuration/steps.rb +121 -0
- data/lib/rails_onboarding/configuration/templates.rb +85 -0
- data/lib/rails_onboarding/configuration/tooltips.rb +32 -0
- data/lib/rails_onboarding/configuration.rb +133 -0
- data/lib/rails_onboarding/configuration_errors.rb +19 -0
- data/lib/rails_onboarding/configuration_validator.rb +477 -0
- data/lib/rails_onboarding/controller_helpers.rb +153 -0
- data/lib/rails_onboarding/current.rb +13 -0
- data/lib/rails_onboarding/deprecation.rb +64 -0
- data/lib/rails_onboarding/devise_integration.rb +122 -0
- data/lib/rails_onboarding/engine.rb +245 -0
- data/lib/rails_onboarding/error_recovery.rb +173 -0
- data/lib/rails_onboarding/lazy_loading.rb +155 -0
- data/lib/rails_onboarding/multi_tenant.rb +306 -0
- data/lib/rails_onboarding/requirements_validator.rb +315 -0
- data/lib/rails_onboarding/responsive_helper.rb +218 -0
- data/lib/rails_onboarding/session_manager.rb +187 -0
- data/lib/rails_onboarding/skip_logic.rb +159 -0
- data/lib/rails_onboarding/turbo_compatibility.rb +229 -0
- data/lib/rails_onboarding/version.rb +3 -0
- data/lib/rails_onboarding.rb +44 -0
- data/lib/tasks/rails_onboarding_analytics.rake +136 -0
- data/lib/tasks/rails_onboarding_tasks.rake +162 -0
- metadata +303 -0
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { Controller } from "@hotwired/stimulus"
|
|
2
|
+
|
|
3
|
+
export default class extends Controller {
|
|
4
|
+
static targets = ["milestoneCard", "pointsValue", "countValue"]
|
|
5
|
+
// Supplied by the view via milestones_path. The engine is mounted at a
|
|
6
|
+
// host-chosen prefix, so this cannot be a literal path.
|
|
7
|
+
static values = { milestonesPath: String }
|
|
8
|
+
|
|
9
|
+
connect() {
|
|
10
|
+
this.animateStats()
|
|
11
|
+
this.setupCardHovers()
|
|
12
|
+
this.checkForNewMilestones()
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
animateStats() {
|
|
16
|
+
// Animate the point counter
|
|
17
|
+
if (this.hasPointsValueTarget) {
|
|
18
|
+
const finalValue = parseInt(this.pointsValueTarget.textContent)
|
|
19
|
+
this.animateCounter(this.pointsValueTarget, finalValue, 1000)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Animate the milestone counter
|
|
23
|
+
if (this.hasCountValueTarget) {
|
|
24
|
+
const finalValue = parseInt(this.countValueTarget.textContent)
|
|
25
|
+
this.animateCounter(this.countValueTarget, finalValue, 800)
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
animateCounter(element, finalValue, duration) {
|
|
30
|
+
const startValue = 0
|
|
31
|
+
const startTime = performance.now()
|
|
32
|
+
|
|
33
|
+
const updateCounter = (currentTime) => {
|
|
34
|
+
const elapsed = currentTime - startTime
|
|
35
|
+
const progress = Math.min(elapsed / duration, 1)
|
|
36
|
+
|
|
37
|
+
// Easing function for smooth animation
|
|
38
|
+
const easeOut = 1 - Math.pow(1 - progress, 3)
|
|
39
|
+
const currentValue = Math.floor(startValue + (finalValue - startValue) * easeOut)
|
|
40
|
+
|
|
41
|
+
element.textContent = currentValue
|
|
42
|
+
|
|
43
|
+
if (progress < 1) {
|
|
44
|
+
requestAnimationFrame(updateCounter)
|
|
45
|
+
} else {
|
|
46
|
+
element.textContent = finalValue
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
requestAnimationFrame(updateCounter)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
setupCardHovers() {
|
|
54
|
+
this.milestoneCardTargets.forEach(card => {
|
|
55
|
+
card.addEventListener('mouseenter', this.handleCardHover.bind(this))
|
|
56
|
+
card.addEventListener('mouseleave', this.handleCardLeave.bind(this))
|
|
57
|
+
})
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
handleCardHover(event) {
|
|
61
|
+
const card = event.currentTarget
|
|
62
|
+
card.style.transform = 'translateY(-4px)'
|
|
63
|
+
card.style.transition = 'transform 0.2s ease-in-out'
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
handleCardLeave(event) {
|
|
67
|
+
const card = event.currentTarget
|
|
68
|
+
card.style.transform = 'translateY(0)'
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
checkForNewMilestones() {
|
|
72
|
+
// Check URL parameters for newly achieved milestones
|
|
73
|
+
const urlParams = new URLSearchParams(window.location.search)
|
|
74
|
+
const awardedMilestones = urlParams.get('awarded_milestones')
|
|
75
|
+
|
|
76
|
+
if (awardedMilestones) {
|
|
77
|
+
const milestoneKeys = awardedMilestones.split(',')
|
|
78
|
+
|
|
79
|
+
// Fetch milestone details and show celebrations
|
|
80
|
+
milestoneKeys.forEach(key => {
|
|
81
|
+
this.fetchAndShowMilestone(key.trim())
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
// Clean up URL
|
|
85
|
+
this.cleanUrl()
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
async fetchAndShowMilestone(milestoneKey) {
|
|
90
|
+
// Without the mounted path there is nothing safe to guess at - bail rather
|
|
91
|
+
// than fire a request at the host app's root.
|
|
92
|
+
if (!this.hasMilestonesPathValue || !this.milestonesPathValue) return
|
|
93
|
+
|
|
94
|
+
const base = this.milestonesPathValue.replace(/\/+$/, "")
|
|
95
|
+
try {
|
|
96
|
+
const response = await fetch(`${base}/${encodeURIComponent(milestoneKey)}`, {
|
|
97
|
+
headers: { 'Accept': 'application/json' }
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
if (response.ok) {
|
|
101
|
+
const milestone = await response.json()
|
|
102
|
+
this.showMilestoneCelebration(milestone)
|
|
103
|
+
}
|
|
104
|
+
} catch (error) {
|
|
105
|
+
console.error('Failed to fetch milestone:', error)
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
showMilestoneCelebration(milestone) {
|
|
110
|
+
// An empty shell on purpose: milestone_celebration_controller builds the
|
|
111
|
+
// same structure the server-rendered partial produces when it connects to
|
|
112
|
+
// an overlay with no .celebration-modal. Attributes are set as properties
|
|
113
|
+
// rather than interpolated into markup so milestone data cannot break out
|
|
114
|
+
// of the attribute.
|
|
115
|
+
const overlay = document.createElement("div")
|
|
116
|
+
overlay.className = "milestone-celebration-overlay"
|
|
117
|
+
overlay.dataset.controller = "milestone-celebration"
|
|
118
|
+
overlay.dataset.milestoneCelebrationMilestoneValue = JSON.stringify(milestone)
|
|
119
|
+
overlay.dataset.temporary = "true"
|
|
120
|
+
if (this.hasMilestonesPathValue && this.milestonesPathValue) {
|
|
121
|
+
overlay.dataset.milestoneCelebrationMilestonesPathValue = this.milestonesPathValue
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
document.body.appendChild(overlay)
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
cleanUrl() {
|
|
128
|
+
const url = new URL(window.location)
|
|
129
|
+
url.searchParams.delete('awarded_milestones')
|
|
130
|
+
window.history.replaceState({}, '', url)
|
|
131
|
+
}
|
|
132
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { Controller } from "@hotwired/stimulus"
|
|
2
|
+
|
|
3
|
+
export default class extends Controller {
|
|
4
|
+
connect() {
|
|
5
|
+
this.setupAnimations()
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
setupAnimations() {
|
|
9
|
+
const iconLarge = this.element.querySelector('.milestone-icon-large')
|
|
10
|
+
const hero = this.element.querySelector('.milestone-hero')
|
|
11
|
+
|
|
12
|
+
if (iconLarge) {
|
|
13
|
+
// Add bounce animation to the icon
|
|
14
|
+
iconLarge.style.animation = 'onboarding-milestone-bounce 1s ease-in-out'
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
if (hero && hero.classList.contains('onboarding-achieved')) {
|
|
18
|
+
// Add celebration effect for achieved milestones
|
|
19
|
+
this.addCelebrationGlow()
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
addCelebrationGlow() {
|
|
24
|
+
const hero = this.element.querySelector('.milestone-hero')
|
|
25
|
+
if (hero) {
|
|
26
|
+
hero.classList.add('celebration-glow')
|
|
27
|
+
|
|
28
|
+
// Remove the glow effect after animation
|
|
29
|
+
setTimeout(() => {
|
|
30
|
+
hero.classList.remove('celebration-glow')
|
|
31
|
+
}, 2000)
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
import { Controller } from "@hotwired/stimulus"
|
|
2
|
+
|
|
3
|
+
export default class extends Controller {
|
|
4
|
+
static targets = ["helpTooltip"]
|
|
5
|
+
static values = { currentStep: String }
|
|
6
|
+
|
|
7
|
+
connect() {
|
|
8
|
+
this.setupKeyboardShortcuts()
|
|
9
|
+
this.setupClickOutsideHandler()
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
// Show help tooltip
|
|
13
|
+
showHelp(event) {
|
|
14
|
+
event.preventDefault()
|
|
15
|
+
|
|
16
|
+
if (this.hasHelpTooltipTarget) {
|
|
17
|
+
this.helpTooltipTarget.hidden = false
|
|
18
|
+
this.positionTooltip()
|
|
19
|
+
this.focusTooltip()
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Hide help tooltip
|
|
24
|
+
hideHelp(event) {
|
|
25
|
+
if (event) event.preventDefault()
|
|
26
|
+
|
|
27
|
+
if (this.hasHelpTooltipTarget) {
|
|
28
|
+
this.helpTooltipTarget.hidden = true
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Handle skip confirmation
|
|
33
|
+
confirmSkip(event) {
|
|
34
|
+
const confirmed = confirm(event.target.dataset.confirm || "Are you sure you want to skip this step?")
|
|
35
|
+
|
|
36
|
+
if (!confirmed) {
|
|
37
|
+
event.preventDefault()
|
|
38
|
+
return false
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Track skip event for analytics
|
|
42
|
+
this.trackSkipEvent()
|
|
43
|
+
return true
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Position help tooltip relative to button
|
|
47
|
+
positionTooltip() {
|
|
48
|
+
if (!this.hasHelpTooltipTarget) return
|
|
49
|
+
|
|
50
|
+
const tooltip = this.helpTooltipTarget
|
|
51
|
+
const button = this.element.querySelector('.help-action')
|
|
52
|
+
|
|
53
|
+
if (button) {
|
|
54
|
+
const buttonRect = button.getBoundingClientRect()
|
|
55
|
+
const tooltipRect = tooltip.getBoundingClientRect()
|
|
56
|
+
|
|
57
|
+
// Position above the button by default
|
|
58
|
+
let top = buttonRect.top - tooltipRect.height - 8
|
|
59
|
+
let left = buttonRect.left + (buttonRect.width / 2) - (tooltipRect.width / 2)
|
|
60
|
+
|
|
61
|
+
// Adjust if tooltip would be off-screen
|
|
62
|
+
if (top < 8) {
|
|
63
|
+
// Position below if no room above
|
|
64
|
+
top = buttonRect.bottom + 8
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (left < 8) {
|
|
68
|
+
left = 8
|
|
69
|
+
} else if (left + tooltipRect.width > window.innerWidth - 8) {
|
|
70
|
+
left = window.innerWidth - tooltipRect.width - 8
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
tooltip.style.position = 'fixed'
|
|
74
|
+
tooltip.style.top = `${top}px`
|
|
75
|
+
tooltip.style.left = `${left}px`
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Focus the first focusable element in tooltip
|
|
80
|
+
focusTooltip() {
|
|
81
|
+
if (this.hasHelpTooltipTarget) {
|
|
82
|
+
const focusableElement = this.helpTooltipTarget.querySelector('button, a, input, [tabindex]')
|
|
83
|
+
if (focusableElement) {
|
|
84
|
+
focusableElement.focus()
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Setup keyboard shortcuts
|
|
90
|
+
setupKeyboardShortcuts() {
|
|
91
|
+
document.addEventListener('keydown', this.handleKeyboard.bind(this))
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Handle keyboard events
|
|
95
|
+
handleKeyboard(event) {
|
|
96
|
+
// Close help on Escape
|
|
97
|
+
if (event.key === 'Escape' && this.hasHelpTooltipTarget && !this.helpTooltipTarget.hidden) {
|
|
98
|
+
this.hideHelp()
|
|
99
|
+
return
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Show help on F1 or Ctrl+?
|
|
103
|
+
if (event.key === 'F1' || (event.ctrlKey && event.key === '?')) {
|
|
104
|
+
event.preventDefault()
|
|
105
|
+
this.showHelp(event)
|
|
106
|
+
return
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Skip with Ctrl+S (if skippable)
|
|
110
|
+
if (event.ctrlKey && event.key === 's') {
|
|
111
|
+
const skipButton = this.element.querySelector('.skip-action')
|
|
112
|
+
if (skipButton) {
|
|
113
|
+
event.preventDefault()
|
|
114
|
+
skipButton.click()
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Setup click outside handler to close tooltip
|
|
120
|
+
setupClickOutsideHandler() {
|
|
121
|
+
document.addEventListener('click', (event) => {
|
|
122
|
+
if (this.hasHelpTooltipTarget && !this.helpTooltipTarget.hidden) {
|
|
123
|
+
// Check if click is outside tooltip and help button
|
|
124
|
+
const tooltip = this.helpTooltipTarget
|
|
125
|
+
const helpButton = this.element.querySelector('.help-action')
|
|
126
|
+
|
|
127
|
+
if (!tooltip.contains(event.target) && !helpButton.contains(event.target)) {
|
|
128
|
+
this.hideHelp()
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
})
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Track skip event for analytics
|
|
135
|
+
trackSkipEvent() {
|
|
136
|
+
// Send analytics event if analytics system is available
|
|
137
|
+
if (typeof gtag !== 'undefined') {
|
|
138
|
+
gtag('event', 'onboarding_step_skipped', {
|
|
139
|
+
step: this.currentStepValue,
|
|
140
|
+
timestamp: new Date().toISOString()
|
|
141
|
+
})
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Send to Rails if needed
|
|
145
|
+
if (typeof Rails !== 'undefined' && Rails.ajax) {
|
|
146
|
+
Rails.ajax({
|
|
147
|
+
url: '/rails_onboarding/analytics/skip',
|
|
148
|
+
type: 'POST',
|
|
149
|
+
data: `step=${encodeURIComponent(this.currentStepValue)}`,
|
|
150
|
+
headers: {
|
|
151
|
+
'Content-Type': 'application/x-www-form-urlencoded',
|
|
152
|
+
'X-CSRF-Token': document.querySelector('meta[name="csrf-token"]')?.content
|
|
153
|
+
}
|
|
154
|
+
})
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Handle navigation button clicks
|
|
159
|
+
navigateToStep(event) {
|
|
160
|
+
const direction = event.params.direction
|
|
161
|
+
const form = document.querySelector('.onboarding-form')
|
|
162
|
+
|
|
163
|
+
if (direction === 'next' && form) {
|
|
164
|
+
// Validate form before proceeding
|
|
165
|
+
const isValid = this.validateCurrentStep()
|
|
166
|
+
if (isValid) {
|
|
167
|
+
form.submit()
|
|
168
|
+
}
|
|
169
|
+
} else if (direction === 'previous') {
|
|
170
|
+
// Handle previous navigation (if implemented)
|
|
171
|
+
this.goToPreviousStep()
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Validate current step before proceeding
|
|
176
|
+
validateCurrentStep() {
|
|
177
|
+
const form = document.querySelector('.onboarding-form')
|
|
178
|
+
if (!form) return true
|
|
179
|
+
|
|
180
|
+
// Check required fields
|
|
181
|
+
const requiredFields = form.querySelectorAll('input[required], select[required]')
|
|
182
|
+
const radioGroups = this.getRadioGroups(form)
|
|
183
|
+
|
|
184
|
+
// Validate radio button groups
|
|
185
|
+
for (const groupName in radioGroups) {
|
|
186
|
+
const group = radioGroups[groupName]
|
|
187
|
+
const hasSelection = group.some(radio => radio.checked)
|
|
188
|
+
|
|
189
|
+
if (!hasSelection && group.length > 0) {
|
|
190
|
+
this.showValidationMessage('Please make a selection to continue.')
|
|
191
|
+
group[0].focus()
|
|
192
|
+
return false
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Validate other required fields
|
|
197
|
+
for (const field of requiredFields) {
|
|
198
|
+
if (field.value.trim() === '') {
|
|
199
|
+
this.showValidationMessage('Please fill in all required fields.')
|
|
200
|
+
field.focus()
|
|
201
|
+
return false
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
return true
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// Get radio button groups
|
|
209
|
+
getRadioGroups(form) {
|
|
210
|
+
const radioGroups = {}
|
|
211
|
+
|
|
212
|
+
form.querySelectorAll('input[type="radio"]').forEach(radio => {
|
|
213
|
+
if (!radioGroups[radio.name]) {
|
|
214
|
+
radioGroups[radio.name] = []
|
|
215
|
+
}
|
|
216
|
+
radioGroups[radio.name].push(radio)
|
|
217
|
+
})
|
|
218
|
+
|
|
219
|
+
return radioGroups
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// Show validation message
|
|
223
|
+
showValidationMessage(message) {
|
|
224
|
+
// Remove existing messages
|
|
225
|
+
document.querySelectorAll('.navigation-validation-error').forEach(error => {
|
|
226
|
+
error.remove()
|
|
227
|
+
})
|
|
228
|
+
|
|
229
|
+
// Create new message
|
|
230
|
+
const errorDiv = document.createElement('div')
|
|
231
|
+
errorDiv.className = 'navigation-validation-error'
|
|
232
|
+
errorDiv.style.cssText = `
|
|
233
|
+
background: #fef2f2;
|
|
234
|
+
border: 1px solid #fecaca;
|
|
235
|
+
color: #dc2626;
|
|
236
|
+
padding: 0.75rem;
|
|
237
|
+
border-radius: 0.5rem;
|
|
238
|
+
margin: 1rem 0;
|
|
239
|
+
text-align: center;
|
|
240
|
+
font-size: 0.875rem;
|
|
241
|
+
`
|
|
242
|
+
errorDiv.textContent = message
|
|
243
|
+
|
|
244
|
+
// Insert at top of navigation
|
|
245
|
+
this.element.insertBefore(errorDiv, this.element.firstChild)
|
|
246
|
+
|
|
247
|
+
// Auto-remove after 5 seconds
|
|
248
|
+
setTimeout(() => {
|
|
249
|
+
if (errorDiv.parentNode) {
|
|
250
|
+
errorDiv.remove()
|
|
251
|
+
}
|
|
252
|
+
}, 5000)
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// Go to previous step (if supported)
|
|
256
|
+
goToPreviousStep() {
|
|
257
|
+
// This would need to be implemented based on your routing
|
|
258
|
+
console.log('Previous step navigation not yet implemented')
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// Show progress summary
|
|
262
|
+
showProgressSummary() {
|
|
263
|
+
const progress = document.querySelector('[data-controller="progress"]')
|
|
264
|
+
if (progress) {
|
|
265
|
+
const currentStep = progress.dataset.progressCurrentStepValue || 1
|
|
266
|
+
const totalSteps = progress.dataset.progressTotalStepsValue || 4
|
|
267
|
+
|
|
268
|
+
alert(`Progress: Step ${currentStep} of ${totalSteps}`)
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// Cleanup on disconnect
|
|
273
|
+
disconnect() {
|
|
274
|
+
document.removeEventListener('keydown', this.handleKeyboard)
|
|
275
|
+
document.removeEventListener('click', this.handleClickOutside)
|
|
276
|
+
}
|
|
277
|
+
}
|