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,511 @@
|
|
|
1
|
+
import { Controller } from "@hotwired/stimulus"
|
|
2
|
+
|
|
3
|
+
// Tooltip Scheduler for Progressive Disclosure
|
|
4
|
+
// Manages sequences of tooltips, timing, and user flow
|
|
5
|
+
export default class extends Controller {
|
|
6
|
+
static values = {
|
|
7
|
+
sequence: String, // JSON array of tooltip configurations
|
|
8
|
+
autoStart: { type: Boolean, default: false },
|
|
9
|
+
delay: { type: Number, default: 1000 }, // Delay between tooltips
|
|
10
|
+
pauseOnHover: { type: Boolean, default: true },
|
|
11
|
+
pauseOnInteraction: { type: Boolean, default: true },
|
|
12
|
+
maxConcurrent: { type: Number, default: 1 }, // Max tooltips shown at once
|
|
13
|
+
priority: { type: String, default: "sequential" } // sequential, priority, adaptive
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
connect() {
|
|
17
|
+
this.currentIndex = 0
|
|
18
|
+
this.isRunning = false
|
|
19
|
+
this.isPaused = false
|
|
20
|
+
this.activeTooltips = []
|
|
21
|
+
this.userInteracting = false
|
|
22
|
+
this.lastInteractionTime = Date.now()
|
|
23
|
+
this.behaviorData = this.loadBehaviorData()
|
|
24
|
+
|
|
25
|
+
this.parseSequence()
|
|
26
|
+
this.setupInteractionTracking()
|
|
27
|
+
|
|
28
|
+
if (this.autoStartValue) {
|
|
29
|
+
this.start()
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Parse the sequence configuration
|
|
34
|
+
parseSequence() {
|
|
35
|
+
try {
|
|
36
|
+
this.tooltipSequence = JSON.parse(this.sequenceValue || '[]')
|
|
37
|
+
} catch (error) {
|
|
38
|
+
console.warn('Invalid tooltip sequence configuration:', error)
|
|
39
|
+
this.tooltipSequence = []
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Validate and enhance each tooltip config
|
|
43
|
+
this.tooltipSequence = this.tooltipSequence.map((config, index) => ({
|
|
44
|
+
id: config.id || `tooltip_${index}`,
|
|
45
|
+
selector: config.selector,
|
|
46
|
+
content: config.content,
|
|
47
|
+
position: config.position || 'top',
|
|
48
|
+
trigger: config.trigger || 'auto',
|
|
49
|
+
priority: config.priority || 5,
|
|
50
|
+
delay: config.delay || this.delayValue,
|
|
51
|
+
duration: config.duration || 5000,
|
|
52
|
+
conditions: config.conditions || {},
|
|
53
|
+
onShow: config.onShow,
|
|
54
|
+
onHide: config.onHide,
|
|
55
|
+
dismissible: config.dismissible !== false,
|
|
56
|
+
contextual: config.contextual || false,
|
|
57
|
+
animation: config.animation || 'fade',
|
|
58
|
+
maxDaily: config.maxDaily || 3,
|
|
59
|
+
...config
|
|
60
|
+
}))
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Setup interaction tracking to pause tooltips during active use
|
|
64
|
+
setupInteractionTracking() {
|
|
65
|
+
// Track various user interactions
|
|
66
|
+
const interactionEvents = ['click', 'keydown', 'scroll', 'mousemove', 'touchstart']
|
|
67
|
+
|
|
68
|
+
interactionEvents.forEach(eventType => {
|
|
69
|
+
document.addEventListener(eventType, () => {
|
|
70
|
+
this.userInteracting = true
|
|
71
|
+
this.lastInteractionTime = Date.now()
|
|
72
|
+
|
|
73
|
+
if (this.pauseOnInteractionValue && this.isRunning) {
|
|
74
|
+
this.pause()
|
|
75
|
+
|
|
76
|
+
// Resume after interaction stops
|
|
77
|
+
clearTimeout(this.resumeTimeout)
|
|
78
|
+
this.resumeTimeout = setTimeout(() => {
|
|
79
|
+
this.userInteracting = false
|
|
80
|
+
if (this.isPaused) {
|
|
81
|
+
this.resume()
|
|
82
|
+
}
|
|
83
|
+
}, 2000) // Resume 2 seconds after interaction stops
|
|
84
|
+
}
|
|
85
|
+
})
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
// Track hovering over tooltips
|
|
89
|
+
if (this.pauseOnHoverValue) {
|
|
90
|
+
document.addEventListener('mouseenter', (event) => {
|
|
91
|
+
if (event.target.closest('.onboarding-tooltip')) {
|
|
92
|
+
this.pause()
|
|
93
|
+
}
|
|
94
|
+
}, true)
|
|
95
|
+
|
|
96
|
+
document.addEventListener('mouseleave', (event) => {
|
|
97
|
+
if (event.target.closest('.onboarding-tooltip')) {
|
|
98
|
+
if (this.isPaused && !this.userInteracting) {
|
|
99
|
+
this.resume()
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}, true)
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Start the tooltip sequence
|
|
107
|
+
start() {
|
|
108
|
+
if (this.isRunning) return
|
|
109
|
+
|
|
110
|
+
this.isRunning = true
|
|
111
|
+
this.isPaused = false
|
|
112
|
+
this.currentIndex = 0
|
|
113
|
+
|
|
114
|
+
this.scheduleNext()
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Pause the sequence
|
|
118
|
+
pause() {
|
|
119
|
+
this.isPaused = true
|
|
120
|
+
if (this.nextTimeout) {
|
|
121
|
+
clearTimeout(this.nextTimeout)
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Resume the sequence
|
|
126
|
+
resume() {
|
|
127
|
+
if (!this.isRunning || !this.isPaused) return
|
|
128
|
+
|
|
129
|
+
this.isPaused = false
|
|
130
|
+
this.scheduleNext()
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Stop the sequence
|
|
134
|
+
stop() {
|
|
135
|
+
this.isRunning = false
|
|
136
|
+
this.isPaused = false
|
|
137
|
+
this.currentIndex = 0
|
|
138
|
+
|
|
139
|
+
if (this.nextTimeout) {
|
|
140
|
+
clearTimeout(this.nextTimeout)
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Hide all active tooltips
|
|
144
|
+
this.hideAllTooltips()
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Schedule the next tooltip in the sequence
|
|
148
|
+
scheduleNext() {
|
|
149
|
+
if (!this.isRunning || this.isPaused) return
|
|
150
|
+
|
|
151
|
+
// Clean up finished tooltips
|
|
152
|
+
this.cleanupActiveTooltips()
|
|
153
|
+
|
|
154
|
+
// Check if we have more tooltips to show
|
|
155
|
+
const nextTooltip = this.getNextTooltip()
|
|
156
|
+
if (!nextTooltip) {
|
|
157
|
+
this.complete()
|
|
158
|
+
return
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Check if we can show more tooltips (concurrent limit)
|
|
162
|
+
if (this.activeTooltips.length >= this.maxConcurrentValue) {
|
|
163
|
+
// Wait for some to finish
|
|
164
|
+
setTimeout(() => this.scheduleNext(), 500)
|
|
165
|
+
return
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Schedule the tooltip
|
|
169
|
+
this.nextTimeout = setTimeout(() => {
|
|
170
|
+
this.showTooltip(nextTooltip)
|
|
171
|
+
this.scheduleNext()
|
|
172
|
+
}, nextTooltip.delay)
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Get the next tooltip to show based on priority strategy
|
|
176
|
+
getNextTooltip() {
|
|
177
|
+
switch (this.priorityValue) {
|
|
178
|
+
case 'sequential':
|
|
179
|
+
return this.getNextSequential()
|
|
180
|
+
case 'priority':
|
|
181
|
+
return this.getNextByPriority()
|
|
182
|
+
case 'adaptive':
|
|
183
|
+
return this.getNextAdaptive()
|
|
184
|
+
default:
|
|
185
|
+
return this.getNextSequential()
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Get next tooltip sequentially
|
|
190
|
+
getNextSequential() {
|
|
191
|
+
while (this.currentIndex < this.tooltipSequence.length) {
|
|
192
|
+
const tooltip = this.tooltipSequence[this.currentIndex]
|
|
193
|
+
this.currentIndex++
|
|
194
|
+
|
|
195
|
+
if (this.shouldShowTooltip(tooltip)) {
|
|
196
|
+
return tooltip
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
return null
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// Get next tooltip by priority (highest first)
|
|
203
|
+
getNextByPriority() {
|
|
204
|
+
const availableTooltips = this.tooltipSequence
|
|
205
|
+
.filter(tooltip => this.shouldShowTooltip(tooltip))
|
|
206
|
+
.sort((a, b) => b.priority - a.priority)
|
|
207
|
+
|
|
208
|
+
if (availableTooltips.length > 0) {
|
|
209
|
+
const tooltip = availableTooltips[0]
|
|
210
|
+
// Remove from sequence so it's not shown again
|
|
211
|
+
this.tooltipSequence = this.tooltipSequence.filter(t => t.id !== tooltip.id)
|
|
212
|
+
return tooltip
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
return null
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// Get next tooltip adaptively based on user behavior
|
|
219
|
+
getNextAdaptive() {
|
|
220
|
+
const behaviorData = this.behaviorData[this.getTodayKey()]
|
|
221
|
+
if (!behaviorData) return this.getNextSequential()
|
|
222
|
+
|
|
223
|
+
// Adapt based on user behavior patterns
|
|
224
|
+
const availableTooltips = this.tooltipSequence.filter(tooltip => this.shouldShowTooltip(tooltip))
|
|
225
|
+
|
|
226
|
+
if (availableTooltips.length === 0) return null
|
|
227
|
+
|
|
228
|
+
// Score tooltips based on user context
|
|
229
|
+
const scoredTooltips = availableTooltips.map(tooltip => ({
|
|
230
|
+
...tooltip,
|
|
231
|
+
score: this.scoreTooltipRelevance(tooltip, behaviorData)
|
|
232
|
+
}))
|
|
233
|
+
|
|
234
|
+
// Sort by score and return highest
|
|
235
|
+
scoredTooltips.sort((a, b) => b.score - a.score)
|
|
236
|
+
const tooltip = scoredTooltips[0]
|
|
237
|
+
|
|
238
|
+
// Remove from sequence
|
|
239
|
+
this.tooltipSequence = this.tooltipSequence.filter(t => t.id !== tooltip.id)
|
|
240
|
+
return tooltip
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// Score tooltip relevance based on user behavior
|
|
244
|
+
scoreTooltipRelevance(tooltip, behaviorData) {
|
|
245
|
+
let score = tooltip.priority
|
|
246
|
+
|
|
247
|
+
// Boost score for struggling areas
|
|
248
|
+
if (behaviorData.strugglingAreas && behaviorData.strugglingAreas.includes(tooltip.selector)) {
|
|
249
|
+
score += 3
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// Reduce score if shown recently
|
|
253
|
+
const recentlyShown = behaviorData.tooltipsShown && behaviorData.tooltipsShown[tooltip.id]
|
|
254
|
+
if (recentlyShown && recentlyShown > Date.now() - 86400000) { // 24 hours
|
|
255
|
+
score -= 2
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// Boost score for high interaction areas
|
|
259
|
+
if (behaviorData.interactions > 10) {
|
|
260
|
+
score += 1
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// Contextual adjustments
|
|
264
|
+
if (tooltip.contextual) {
|
|
265
|
+
// Check if user is on relevant page/section
|
|
266
|
+
const relevantElement = document.querySelector(tooltip.selector)
|
|
267
|
+
if (relevantElement && this.isElementVisible(relevantElement)) {
|
|
268
|
+
score += 2
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
return score
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// Check if tooltip should be shown
|
|
276
|
+
shouldShowTooltip(tooltip) {
|
|
277
|
+
// Check if element exists
|
|
278
|
+
const element = document.querySelector(tooltip.selector)
|
|
279
|
+
if (!element) return false
|
|
280
|
+
|
|
281
|
+
// Check visibility
|
|
282
|
+
if (!this.isElementVisible(element)) return false
|
|
283
|
+
|
|
284
|
+
// Check conditions
|
|
285
|
+
if (tooltip.conditions && !this.checkConditions(tooltip.conditions)) {
|
|
286
|
+
return false
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// Check daily limits
|
|
290
|
+
const behaviorData = this.behaviorData[this.getTodayKey()]
|
|
291
|
+
if (behaviorData && behaviorData.tooltipsShown) {
|
|
292
|
+
const todayCount = behaviorData.tooltipsShown[tooltip.id] || 0
|
|
293
|
+
if (todayCount >= tooltip.maxDaily) {
|
|
294
|
+
return false
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// Check if already dismissed
|
|
299
|
+
const seenTooltips = JSON.parse(localStorage.getItem('rails_onboarding_seen_tooltips') || '{}')
|
|
300
|
+
if (seenTooltips[tooltip.id]) {
|
|
301
|
+
return false
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
return true
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// Check custom conditions
|
|
308
|
+
checkConditions(conditions) {
|
|
309
|
+
for (const [condition, value] of Object.entries(conditions)) {
|
|
310
|
+
switch (condition) {
|
|
311
|
+
case 'url':
|
|
312
|
+
if (!window.location.href.includes(value)) return false
|
|
313
|
+
break
|
|
314
|
+
case 'element_visible':
|
|
315
|
+
if (!this.isElementVisible(document.querySelector(value))) return false
|
|
316
|
+
break
|
|
317
|
+
case 'time_on_page':
|
|
318
|
+
const timeOnPage = Date.now() - this.pageStartTime
|
|
319
|
+
if (timeOnPage < value) return false
|
|
320
|
+
break
|
|
321
|
+
case 'user_type':
|
|
322
|
+
// Could check user attributes here
|
|
323
|
+
break
|
|
324
|
+
case 'screen_size':
|
|
325
|
+
if (value === 'mobile' && window.innerWidth > 768) return false
|
|
326
|
+
if (value === 'desktop' && window.innerWidth <= 768) return false
|
|
327
|
+
break
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
return true
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// Show a tooltip
|
|
334
|
+
showTooltip(tooltipConfig) {
|
|
335
|
+
const element = document.querySelector(tooltipConfig.selector)
|
|
336
|
+
if (!element) return
|
|
337
|
+
|
|
338
|
+
// Create tooltip controller instance
|
|
339
|
+
const tooltipElement = this.createTooltipElement(tooltipConfig)
|
|
340
|
+
element.appendChild(tooltipElement)
|
|
341
|
+
|
|
342
|
+
// Track as active
|
|
343
|
+
const tooltipInstance = {
|
|
344
|
+
config: tooltipConfig,
|
|
345
|
+
element: tooltipElement,
|
|
346
|
+
startTime: Date.now()
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
this.activeTooltips.push(tooltipInstance)
|
|
350
|
+
this.recordTooltipShow(tooltipConfig.id)
|
|
351
|
+
|
|
352
|
+
// Auto-hide after duration
|
|
353
|
+
setTimeout(() => {
|
|
354
|
+
this.hideTooltip(tooltipInstance)
|
|
355
|
+
}, tooltipConfig.duration)
|
|
356
|
+
|
|
357
|
+
// Execute onShow callback
|
|
358
|
+
if (tooltipConfig.onShow && typeof window[tooltipConfig.onShow] === 'function') {
|
|
359
|
+
window[tooltipConfig.onShow](tooltipConfig)
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
// Create tooltip DOM element
|
|
364
|
+
createTooltipElement(config) {
|
|
365
|
+
const wrapper = document.createElement('div')
|
|
366
|
+
wrapper.innerHTML = `
|
|
367
|
+
<div data-controller="tooltip"
|
|
368
|
+
data-tooltip-position-value="${config.position}"
|
|
369
|
+
data-tooltip-feature-value="${config.id}"
|
|
370
|
+
data-tooltip-animation-value="${config.animation}"
|
|
371
|
+
data-tooltip-trigger-value="auto"
|
|
372
|
+
data-tooltip-dismissible-value="${config.dismissible}">
|
|
373
|
+
<!-- hidden: the tooltip controller reads this element's innerHTML to
|
|
374
|
+
build the floating popup, so it must exist in the DOM, but it must
|
|
375
|
+
not render inline inside the target element (which bled the step's
|
|
376
|
+
copy into the page, e.g. the dashboard title). -->
|
|
377
|
+
<div data-tooltip-target="content" hidden>
|
|
378
|
+
${config.content}
|
|
379
|
+
</div>
|
|
380
|
+
</div>
|
|
381
|
+
`
|
|
382
|
+
return wrapper.firstElementChild
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
// Hide a specific tooltip
|
|
386
|
+
hideTooltip(tooltipInstance) {
|
|
387
|
+
if (tooltipInstance.element && tooltipInstance.element.parentNode) {
|
|
388
|
+
// Trigger hide through tooltip controller
|
|
389
|
+
const tooltipController = this.application.getControllerForElementAndIdentifier(
|
|
390
|
+
tooltipInstance.element, 'tooltip'
|
|
391
|
+
)
|
|
392
|
+
|
|
393
|
+
if (tooltipController) {
|
|
394
|
+
tooltipController.forceHide()
|
|
395
|
+
} else {
|
|
396
|
+
tooltipInstance.element.remove()
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
// Execute onHide callback
|
|
401
|
+
if (tooltipInstance.config.onHide && typeof window[tooltipInstance.config.onHide] === 'function') {
|
|
402
|
+
window[tooltipInstance.config.onHide](tooltipInstance.config)
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
// Remove from active tooltips
|
|
406
|
+
this.activeTooltips = this.activeTooltips.filter(t => t !== tooltipInstance)
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
// Hide all active tooltips
|
|
410
|
+
hideAllTooltips() {
|
|
411
|
+
[...this.activeTooltips].forEach(tooltip => {
|
|
412
|
+
this.hideTooltip(tooltip)
|
|
413
|
+
})
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
// Clean up finished tooltips
|
|
417
|
+
cleanupActiveTooltips() {
|
|
418
|
+
this.activeTooltips = this.activeTooltips.filter(tooltip => {
|
|
419
|
+
return tooltip.element && tooltip.element.parentNode
|
|
420
|
+
})
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
// Check if element is visible
|
|
424
|
+
isElementVisible(element) {
|
|
425
|
+
if (!element) return false
|
|
426
|
+
|
|
427
|
+
const rect = element.getBoundingClientRect()
|
|
428
|
+
return rect.width > 0 && rect.height > 0 &&
|
|
429
|
+
rect.top >= 0 && rect.left >= 0 &&
|
|
430
|
+
rect.bottom <= window.innerHeight &&
|
|
431
|
+
rect.right <= window.innerWidth
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
// Complete the sequence
|
|
435
|
+
complete() {
|
|
436
|
+
this.isRunning = false
|
|
437
|
+
this.isPaused = false
|
|
438
|
+
|
|
439
|
+
// Dispatch completion event
|
|
440
|
+
this.dispatch('complete', {
|
|
441
|
+
detail: {
|
|
442
|
+
totalShown: this.activeTooltips.length,
|
|
443
|
+
sequence: this.tooltipSequence
|
|
444
|
+
}
|
|
445
|
+
})
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
// Load behavior data
|
|
449
|
+
loadBehaviorData() {
|
|
450
|
+
return JSON.parse(localStorage.getItem('rails_onboarding_behavior') || '{}')
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
// Record tooltip show
|
|
454
|
+
recordTooltipShow(tooltipId) {
|
|
455
|
+
const todayKey = this.getTodayKey()
|
|
456
|
+
|
|
457
|
+
if (!this.behaviorData[todayKey]) {
|
|
458
|
+
this.behaviorData[todayKey] = { tooltipsShown: {} }
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
if (!this.behaviorData[todayKey].tooltipsShown) {
|
|
462
|
+
this.behaviorData[todayKey].tooltipsShown = {}
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
// Store a running COUNT, not a timestamp: shouldShowTooltip compares this
|
|
466
|
+
// against tooltip.maxDaily as a count, so storing Date.now() here made every
|
|
467
|
+
// value astronomically larger than maxDaily and suppressed the tooltip after
|
|
468
|
+
// a single show (breaking the guided tour on any subsequent load).
|
|
469
|
+
const shownToday = this.behaviorData[todayKey].tooltipsShown
|
|
470
|
+
shownToday[tooltipId] = (shownToday[tooltipId] || 0) + 1
|
|
471
|
+
|
|
472
|
+
localStorage.setItem('rails_onboarding_behavior', JSON.stringify(this.behaviorData))
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
// Get today's key
|
|
476
|
+
getTodayKey() {
|
|
477
|
+
return new Date().toISOString().split('T')[0]
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
// Public API methods
|
|
481
|
+
skipCurrent() {
|
|
482
|
+
if (this.activeTooltips.length > 0) {
|
|
483
|
+
this.hideTooltip(this.activeTooltips[0])
|
|
484
|
+
}
|
|
485
|
+
this.scheduleNext()
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
skipAll() {
|
|
489
|
+
this.stop()
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
showTooltipById(id) {
|
|
493
|
+
const tooltip = this.tooltipSequence.find(t => t.id === id)
|
|
494
|
+
if (tooltip && this.shouldShowTooltip(tooltip)) {
|
|
495
|
+
this.showTooltip(tooltip)
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
// Cleanup on disconnect
|
|
500
|
+
disconnect() {
|
|
501
|
+
this.stop()
|
|
502
|
+
|
|
503
|
+
if (this.nextTimeout) {
|
|
504
|
+
clearTimeout(this.nextTimeout)
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
if (this.resumeTimeout) {
|
|
508
|
+
clearTimeout(this.resumeTimeout)
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
}
|