PluginProbe
The Innovative Form Builder – IvyForms / 0.7
The Innovative Form Builder – IvyForms v0.7
1.5.1 1.5 1.4.1 1.4 trunk 0.1.2 0.2 0.2.1 0.3 0.3.1 0.4 0.5 0.6 0.6.1 0.6.1-backup 0.6.1.1 0.7 0.8 0.8.1 0.8.2 0.9 0.9.1 1.0 1.1 1.1.1 All 27 releases
ivyforms / frontend / assets / js / admin-deactivation-modal.js

admin-deactivation-modal.js in The Innovative Form Builder – IvyForms 0.7, at frontend/assets/js/admin-deactivation-modal.js

411 lines 16.0 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 document.addEventListener('DOMContentLoaded', () => {
2 // Helper function to get WordPress base path
3 const getWpAdminBasePath = () => {
4 const pathname = window.location.pathname
5 const wpAdminIndex = pathname.indexOf('/wp-admin/')
6 if (wpAdminIndex !== -1) {
7 const basePath = pathname.substring(0, wpAdminIndex)
8 return window.location.origin + basePath + '/wp-admin/'
9 }
10 // Fallback: remove trailing filename and use current directory structure
11 const pathParts = pathname.split('/').filter((p) => p.length > 0)
12 if (pathParts.length > 0) {
13 // Remove last part (filename) and reconstruct path
14 pathParts.pop()
15 return window.location.origin + '/' + pathParts.join('/') + '/wp-admin/'
16 }
17 return window.location.origin + '/wp-admin/'
18 }
19
20 const wpAdminBasePath = getWpAdminBasePath()
21
22 const ivyformsRow = document.querySelector('tr[data-slug="ivyforms"]')
23 if (!ivyformsRow) return
24 const deactivateLink = ivyformsRow.querySelector('.deactivate a')
25 if (!deactivateLink) return
26 window.ivyformsDeactivateUrl = deactivateLink.href
27 deactivateLink.addEventListener('click', (e) => {
28 e.preventDefault()
29 showIvyDeactivationModal()
30 })
31
32
33 // Helper: create element with props
34 function el(tag, props = {}, ...children) {
35 const node = document.createElement(tag)
36 Object.entries(props).forEach(([k, v]) => {
37 if (k === 'style' && typeof v === 'object') Object.assign(node.style, v)
38 else if (k.startsWith('on') && typeof v === 'function') node[k] = v
39 else node[k] = v
40 })
41 children.forEach((child) => {
42 if (child)
43 node.appendChild(typeof child === 'string' ? document.createTextNode(child) : child)
44 })
45 return node
46 }
47
48 function showIvyDeactivationModal() {
49 const pluginUrl =
50 typeof window.ivyformsData !== 'undefined' && window.ivyformsData.pluginUrl
51 ? window.ivyformsData.pluginUrl
52 : ''
53 document.getElementById('ivyforms-deactivation-modal-mount')?.remove()
54 const overlay = el('div', {
55 id: 'ivyforms-deactivation-modal-mount',
56 style: {
57 position: 'fixed',
58 top: 0,
59 left: 0,
60 width: '100vw',
61 height: '100vh',
62 background: 'rgba(0,0,0,0.15)',
63 display: 'flex',
64 alignItems: 'center',
65 justifyContent: 'center',
66 zIndex: 9999,
67 },
68 })
69 const modal = el('div', {
70 style: {
71 background: '#fff',
72 borderRadius: '12px',
73 boxShadow: '0 10px 25px rgba(0,0,0,0.15)',
74 width: '420px',
75 maxWidth: '95vw',
76 padding: '40px 32px',
77 position: 'relative',
78 fontFamily: 'inherit',
79 },
80 })
81 // Scoped styles
82 modal.appendChild(
83 el(
84 'style',
85 { type: 'text/css' },
86 `
87 #ivyforms-deactivation-modal-mount { --checkbox-border: #a78bfa; --checkbox-bg: #f3f0ff; --checkbox-checked: #7c3aed; --checkbox-checked-hover: #6d28d9; }
88 #ivyforms-deactivation-modal-mount .ivy-option-row { display: flex; flex-direction: row; align-items: center; gap: 12px; min-height: 24px; background: transparent !important; margin: 0; padding: 10px 0; transition: background 0.15s; }
89 #ivyforms-deactivation-modal-mount .ivy-option-row:last-child { border-bottom: none; }
90 #ivyforms-deactivation-modal-mount .ivy-option-row:hover { background: #fafafa !important; }
91 #ivyforms-deactivation-modal-mount .ivy-option-row label { display: flex; flex-direction: row; align-items: center; gap: 12px; width: 100%; font-size: 14px; color: #1f2937; font-weight: 500; cursor: pointer; margin: 0; padding: 0; height: unset; min-height: 20px; white-space: normal; position: relative; }
92 #ivyforms-deactivation-modal-mount .ivy-option-row input[type="checkbox"] { position: absolute; opacity: 0; width: 0; height: 0; cursor: pointer; margin: 0; padding: 0; }
93 #ivyforms-deactivation-modal-mount .ivy-custom-checkbox { display: inline-flex; align-items: center; justify-content: center; width: 20px; height: 20px; border-radius: 4px; border: 1.5px solid #C0C5CB; box-sizing: border-box; flex-shrink: 0; cursor: pointer; transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1); position: relative; -webkit-tap-highlight-color: transparent; }
94 #ivyforms-deactivation-modal-mount .ivy-option-row:hover .ivy-custom-checkbox:not(.checked) { background: var(--checkbox-hover-bg); border-color: #c4b5fd; }
95 #ivyforms-deactivation-modal-mount .ivy-custom-checkbox.checked { background: var(--checkbox-checked); border-color: var(--checkbox-checked); }
96 #ivyforms-deactivation-modal-mount .ivy-option-row:hover .ivy-custom-checkbox.checked { background: var(--checkbox-checked-hover); border-color: var(--checkbox-checked-hover); }
97 #ivyforms-deactivation-modal-mount .ivy-custom-checkbox svg { display: block; width: 12px; height: 10px; flex-shrink: 0; }
98 #ivyforms-deactivation-modal-mount .ivy-custom-checkbox:focus-visible { outline: 2px solid #7c3aed; outline-offset: 2px; }
99 #ivyforms-deactivation-modal-mount .ivyforms-modal-btn { border-radius: 6px; font-size: 15px; font-weight: 600; padding: 10px 20px; cursor: pointer; border: 1.5px solid #e5e7eb; transition: all 0.2s ease; min-height: 40px; display: inline-flex; align-items: center; justify-content: center; white-space: nowrap; -webkit-tap-highlight-color: transparent; }
100 #ivyforms-deactivation-modal-mount .ivyforms-modal-btn.skip { background: #f9fafb; color: #4b5563; border-color: #e5e7eb; }
101 #ivyforms-deactivation-modal-mount .ivyforms-modal-btn.skip:hover { background: #f3f4f6; border-color: #d1d5db; }
102 #ivyforms-deactivation-modal-mount .ivyforms-modal-btn.submit { background: #06A192; color: #fff; border-color: #06A192; }
103 #ivyforms-deactivation-modal-mount .ivyforms-modal-btn.submit:hover { background: #036a73; border-color: #036a73; }
104 `,
105 ),
106 )
107 // Close button
108 modal.appendChild(
109 el('button', {
110 innerHTML: '×',
111 style: {
112 position: 'absolute',
113 top: '16px',
114 right: '16px',
115 background: 'none',
116 border: 'none',
117 fontSize: '24px',
118 cursor: 'pointer',
119 color: '#888',
120 },
121 onclick: () => overlay.remove(),
122 }),
123 )
124 // Mascot
125 const mascotWrapper = el('div', {
126 style: { width: '120px', margin: '0 auto 20px', display: 'block' },
127 })
128 mascotWrapper.innerHTML = '<div class="mascot-placeholder" aria-hidden="true"></div>'
129 fetch(pluginUrl + '/frontend/assets/img/mascot/IvyMascot.svg')
130 .then((r) => (r.ok ? r.text() : ''))
131 .then((svg) => {
132 mascotWrapper.innerHTML = svg
133 ? svg.replace(/xmlns=["']https?:\/\/www\.w3\.org\/2000\/svg["']/g, '')
134 : ''
135 })
136 modal.appendChild(mascotWrapper)
137 // Title & desc
138 modal.appendChild(
139 el(
140 'h2',
141 { style: { marginBottom: '10px', textAlign: 'center', fontWeight: 600, fontSize: '20px' } },
142 "We're sorry to see you go!",
143 ),
144 )
145 modal.appendChild(
146 el(
147 'p',
148 { style: { marginBottom: '20px', textAlign: 'center', fontSize: '15px', color: '#444' } },
149 "Can you tell us why you're deactivating the plugin?",
150 ),
151 )
152 // Options
153 const options = [
154 { label: 'I no longer need it', value: 'no_need' },
155 { label: 'I found a better plugin', value: 'better_plugin' },
156 { label: 'I experienced issue/bugs', value: 'bugs' },
157 { label: 'Missing Features', value: 'missing_features' },
158 { label: 'Temporary deactivation', value: 'temporary' },
159 { label: 'Other', value: 'other' },
160 ]
161 let checked = {},
162 textareaDiv = null
163 const optionsBox = el('div', {
164 style: {
165 border: 'none',
166 borderRadius: 0,
167 background: 'transparent',
168 padding: 0,
169 marginBottom: 0,
170 display: 'flex',
171 flexDirection: 'column',
172 gap: 0,
173 },
174 })
175 options.forEach((opt) => {
176 const row = el('div', { className: 'ivy-option-row' })
177 const label = el('label')
178 const input = el('input', {
179 type: 'checkbox',
180 value: opt.value,
181 style: {
182 position: 'absolute',
183 opacity: 0,
184 width: '16px',
185 height: '16px',
186 left: 0,
187 top: '50%',
188 transform: 'translateY(-50%)',
189 },
190 })
191 const customBox = el('span', {
192 className: 'ivy-custom-checkbox',
193 role: 'checkbox',
194 tabIndex: 0,
195 'aria-checked': 'false',
196 })
197 function renderCustomChecked(el, isChecked) {
198 if (isChecked) {
199 el.classList.add('checked')
200 el.setAttribute('aria-checked', 'true')
201 el.innerHTML =
202 '<svg width="12" height="10" viewBox="0 0 12 10" style="display:block;margin:2px"><path d="M1 5L4 8L11 1" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>'
203 } else {
204 el.classList.remove('checked')
205 el.setAttribute('aria-checked', 'false')
206 el.innerHTML = ''
207 }
208 }
209 input.addEventListener('change', () => {
210 if (input.checked) {
211 options.forEach((otherOpt) => {
212 if (otherOpt.value !== opt.value) checked[otherOpt.value] = false
213 })
214 document
215 .querySelectorAll('#ivyforms-deactivation-modal-mount .ivy-custom-checkbox')
216 .forEach((box) => {
217 if (box !== customBox && box.classList.contains('checked')) {
218 box.classList.remove('checked')
219 box.setAttribute('aria-checked', 'false')
220 box.innerHTML = ''
221 const correspondingInput = box.parentElement.querySelector('input[type="checkbox"]')
222 if (correspondingInput) correspondingInput.checked = false
223 }
224 })
225 }
226 checked[opt.value] = input.checked
227 if (opt.value === 'other' && textareaDiv)
228 textareaDiv.style.display = input.checked ? 'block' : 'none'
229 renderCustomChecked(customBox, input.checked)
230 })
231 customBox.addEventListener('click', (ev) => {
232 ev.preventDefault()
233 input.checked = !input.checked
234 input.dispatchEvent(new Event('change', { bubbles: true }))
235 })
236 customBox.addEventListener('keydown', (ev) => {
237 if (ev.key === ' ' || ev.key === 'Enter') {
238 ev.preventDefault()
239 input.checked = !input.checked
240 input.dispatchEvent(new Event('change', { bubbles: true }))
241 }
242 })
243 label.appendChild(input)
244 label.appendChild(customBox)
245 label.appendChild(el('span', {}, opt.label))
246 row.appendChild(label)
247 optionsBox.appendChild(row)
248 renderCustomChecked(customBox, !!input.checked)
249 })
250 // Textarea for 'Other'
251 textareaDiv = el(
252 'div',
253 { style: { marginTop: '16px', display: 'none' } },
254 el('textarea', {
255 placeholder: 'Write your comment here',
256 style: {
257 width: '100%',
258 height: '80px',
259 resize: 'none',
260 borderRadius: '6px',
261 border: '1px solid #e5e7eb',
262 padding: '12px',
263 fontSize: '14px',
264 boxSizing: 'border-box',
265 fontFamily: 'inherit',
266 },
267 }),
268 )
269 const groupContainer = el(
270 'div',
271 { style: { marginBottom: '20px', textAlign: 'left' } },
272 optionsBox,
273 textareaDiv,
274 )
275
276 // Create notice container (will be populated if delete_on_uninstall is enabled)
277 const noticeContainer = el('div', { id: 'ivy-delete-notice-container' })
278
279 // Add notice BEFORE options (so it appears at the top)
280 modal.appendChild(noticeContainer)
281 modal.appendChild(groupContainer)
282
283 // Check delete_on_uninstall setting and show notice if enabled
284 const deleteOnUninstall =
285 typeof window.ivyformsData !== 'undefined' && window.ivyformsData.deleteOnUninstall
286 ? window.ivyformsData.deleteOnUninstall
287 : false
288
289 // Check if enabled (handle true, "true", 1, "1")
290 const isDeleteOnUninstallEnabled = deleteOnUninstall === true || deleteOnUninstall === 1 || deleteOnUninstall === '1' || deleteOnUninstall === 'true'
291
292 if (isDeleteOnUninstallEnabled) {
293 // Get translations
294 const i18n = (typeof window.ivyformsData !== 'undefined' && window.ivyformsData.i18n) ? window.ivyformsData.i18n : {}
295 const warningTitle = i18n.warningTitle || 'Warning'
296 const warningMessage = i18n.warningMessage || 'All IvyForms database tables and settings will be deleted when this plugin is deactivated. To prevent this,'
297 const warningLinkText = i18n.warningLinkText || 'disable this option in settings'
298
299 // Show notice if delete_on_uninstall is enabled
300 const settingsUrl = wpAdminBasePath + 'admin.php?page=ivyforms-settings#/general/'
301 const noticeBox = el('div', {
302 style: {
303 background: '#FEF3C7',
304 border: '1px solid #F59E0B',
305 borderRadius: '6px',
306 padding: '12px 16px',
307 marginBottom: '20px',
308 fontSize: '14px',
309 color: '#92400E',
310 lineHeight: '1.5',
311 },
312 })
313 const noticeText = el('p', {
314 style: { margin: 0 },
315 })
316 noticeText.innerHTML = `<strong>${warningTitle}:</strong> ${warningMessage} `
317 const settingsLink = el('a', {
318 href: settingsUrl,
319 textContent: warningLinkText,
320 style: {
321 color: '#D97706',
322 textDecoration: 'underline',
323 fontWeight: '600',
324 cursor: 'pointer',
325 },
326 target: '_blank',
327 rel: 'noopener noreferrer',
328 })
329 noticeText.appendChild(settingsLink)
330 noticeText.appendChild(document.createTextNode('.'))
331 noticeBox.appendChild(noticeText)
332 noticeContainer.appendChild(noticeBox)
333 }
334
335 // Buttons
336 const btnRow = el('div', {
337 style: {
338 display: 'flex',
339 flexDirection: 'row',
340 alignItems: 'center',
341 justifyContent: 'flex-end',
342 gap: '12px',
343 marginTop: '24px',
344 paddingTop: '16px',
345 borderTop: '1px solid #e5e7eb',
346 },
347 })
348 btnRow.appendChild(
349 el('button', {
350 className: 'ivyforms-modal-btn skip',
351 textContent: 'Skip & Deactivate',
352 onclick: () => {
353 window.location.href = window.ivyformsDeactivateUrl
354 },
355 }),
356 )
357 btnRow.appendChild(
358 el('button', {
359 className: 'ivyforms-modal-btn submit',
360 textContent: 'Submit & Deactivate',
361 onclick: () => {
362 const selectedReasons = Object.keys(checked).filter((k) => checked[k])
363 const selectedReason = selectedReasons.length > 0 ? selectedReasons[0] : ''
364 const feedbackComment = checked['other']
365 ? textareaDiv.querySelector('textarea').value
366 : ''
367 const pluginVersion =
368 typeof window.ivyformsData !== 'undefined' && window.ivyformsData.version
369 ? window.ivyformsData.version
370 : ''
371 const restApiUrl =
372 typeof window.ivyformsData !== 'undefined' && window.ivyformsData.restApiUrl
373 ? window.ivyformsData.restApiUrl
374 : ''
375
376 // Always deactivate - helper function
377 const deactivatePlugin = () => {
378 window.location.href = window.ivyformsDeactivateUrl
379 }
380
381 // Try to send feedback, but always deactivate regardless of result
382 if (restApiUrl && selectedReason) {
383 fetch(restApiUrl, {
384 method: 'POST',
385 headers: {
386 'Content-Type': 'application/json',
387 'X-WP-Nonce':
388 typeof window.ivyformsData !== 'undefined' && window.ivyformsData.restNonce
389 ? window.ivyformsData.restNonce
390 : '',
391 },
392 body: JSON.stringify({
393 api_version: pluginVersion,
394 feedback_key: selectedReason,
395 feedback: feedbackComment,
396 }),
397 })
398 .then(() => deactivatePlugin())
399 .catch(() => deactivatePlugin()) // Always deactivate even on error
400 } else {
401 deactivatePlugin()
402 }
403 },
404 }),
405 )
406 modal.appendChild(btnRow)
407 overlay.appendChild(modal)
408 document.body.appendChild(overlay)
409 }
410 })
411