PluginProbe
The Innovative Form Builder – IvyForms / 1.0
The Innovative Form Builder – IvyForms v1.0
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 1.0, at frontend/assets/js/admin-deactivation-modal.js

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