PluginProbe
Extendify / 0.5.0
Extendify v0.5.0
3.2.1 3.2.0 3.1.6 3.1.5 3.1.4 3.1.3 3.1.2 3.1.1 3.1.0 3.0.6 3.0.5 3.0.4 trunk 0.1.0 0.10.0 0.10.1 0.10.2 0.11.0 0.11.1 0.2.0 0.3.0 0.3.1 0.4.0 0.5.0 0.6.0 All 127 releases
extendify / src / components / ImportTemplateBlock.js

ImportTemplateBlock.js in Extendify 0.5.0, at src/components/ImportTemplateBlock.js

206 lines 8.2 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 import { BlockPreview } from '@wordpress/block-editor'
2 import { rawHandler } from '@wordpress/blocks'
3 import { useEffect, useState, useRef, useMemo } from '@wordpress/element'
4 import { __, sprintf } from '@wordpress/i18n'
5 import classNames from 'classnames'
6 import { Templates as TemplatesApi } from '@extendify/api/Templates'
7 import { useIsDevMode } from '@extendify/hooks/helpers'
8 import { AuthorizationCheck, Middleware } from '@extendify/middleware'
9 import { useGlobalStore } from '@extendify/state/GlobalState'
10 import { useUserStore } from '@extendify/state/User'
11 import { injectTemplateBlocks } from '@extendify/util/templateInjection'
12 import { DevButtonOverlay } from './DevHelpers'
13 import { NoImportModal } from './modals/NoImportModal'
14 import { ProModal } from './modals/ProModal'
15
16 const canImportMiddleware = Middleware([
17 'hasRequiredPlugins',
18 'hasPluginsActivated',
19 ])
20
21 export function ImportTemplateBlock({ template, maxHeight }) {
22 const importButtonRef = useRef(null)
23 const once = useRef(false)
24 const hasAvailableImports = useUserStore(
25 (state) => state.hasAvailableImports,
26 )
27 const loggedIn = useUserStore((state) => state.apiKey.length)
28 const setOpen = useGlobalStore((state) => state.setOpen)
29 const pushModal = useGlobalStore((state) => state.pushModal)
30 const removeAllModals = useGlobalStore((state) => state.removeAllModals)
31 const blocks = useMemo(
32 () => rawHandler({ HTML: template.fields.code }),
33 [template.fields.code],
34 )
35 const [loaded, setLoaded] = useState(false)
36 const devMode = useIsDevMode()
37 const [topValue, setTopValue] = useState(0)
38
39 const focusTrapInnerBlocks = () => {
40 if (once.current) return
41 once.current = true
42 Array.from(
43 importButtonRef.current.querySelectorAll(
44 'a, button, input, textarea, select, details, [tabindex]:not([tabindex="-1"])',
45 ),
46 ).forEach((el) => el.setAttribute('tabIndex', '-1'))
47 }
48
49 const importTemplates = async () => {
50 await canImportMiddleware.check(template)
51 AuthorizationCheck(canImportMiddleware)
52 .then(() => {
53 setTimeout(() => {
54 injectTemplateBlocks(blocks, template)
55 .then(() => removeAllModals())
56 .then(() => setOpen(false))
57 .then(() => canImportMiddleware.reset())
58 }, 100)
59 })
60 .catch(() => {})
61 }
62
63 const handleKeyDown = (event) => {
64 if (['Enter', 'Space', ' '].includes(event.key)) {
65 event.stopPropagation()
66 event.preventDefault()
67 importTemplate()
68 }
69 }
70
71 const importTemplate = () => {
72 // Make a note that they attempted to import
73 TemplatesApi.maybeImport(template)
74
75 if (template?.fields?.pro && !loggedIn) {
76 pushModal(<ProModal />)
77 return
78 }
79 if (!hasAvailableImports()) {
80 pushModal(<NoImportModal />)
81 return
82 }
83
84 importTemplates()
85 }
86
87 // Trigger resize event on the live previews to add
88 // Grammerly/Loom/etc compatability
89 // TODO: This can probably be removed after WP 5.9
90 useEffect(() => {
91 const rafIds = []
92 const timeouts = []
93 let rafId1, rafId2, rafId3, rafId4
94 rafId1 = window.requestAnimationFrame(() => {
95 rafId2 = window.requestAnimationFrame(() => {
96 importButtonRef.current
97 .querySelectorAll('iframe')
98 .forEach((frame) => {
99 const inner = frame.contentWindow.document.body
100 const rafId = window.requestAnimationFrame(() => {
101 const maybeRoot =
102 inner.querySelector('.is-root-container')
103 if (maybeRoot) {
104 const height = maybeRoot?.offsetHeight
105 if (height) {
106 rafId4 = window.requestAnimationFrame(
107 () => {
108 frame.contentWindow.dispatchEvent(
109 new Event('resize'),
110 )
111 },
112 )
113 const id = window.setTimeout(() => {
114 frame.contentWindow.dispatchEvent(
115 new Event('resize'),
116 )
117 }, 2000)
118 timeouts.push(id)
119 }
120 }
121 frame.contentWindow.dispatchEvent(
122 new Event('resize'),
123 )
124 })
125 rafIds.push(rafId)
126 })
127 rafId3 = window.requestAnimationFrame(() => {
128 window.dispatchEvent(new Event('resize'))
129 setLoaded(true)
130 })
131 })
132 })
133 return () => {
134 ;[...rafIds, rafId1, rafId2, rafId3, rafId4].forEach((id) =>
135 window.cancelAnimationFrame(id),
136 )
137 timeouts.forEach((id) => window.clearTimeout(id))
138 }
139 }, [])
140
141 useEffect(() => {
142 if (!Number.isInteger(maxHeight)) return
143 const button = importButtonRef.current
144 const handleIn = () => {
145 // The live component changes over time so easier to query on demand
146 const height = button.offsetHeight
147 button.style.transitionDuration = height * 1.5 + 'ms'
148 setTopValue(Math.abs(height - maxHeight) * -1)
149 }
150 const handleOut = () => {
151 const height = button.offsetHeight
152 button.style.transitionDuration = height / 1.5 + 'ms'
153 setTopValue(0)
154 }
155 button.addEventListener('focus', handleIn)
156 button.addEventListener('mouseenter', handleIn)
157 button.addEventListener('blur', handleOut)
158 button.addEventListener('mouseleave', handleOut)
159 return () => {
160 button.removeEventListener('focus', handleIn)
161 button.removeEventListener('mouseenter', handleIn)
162 button.removeEventListener('blur', handleOut)
163 button.removeEventListener('mouseleave', handleOut)
164 }
165 }, [maxHeight])
166
167 return (
168 <div className="group relative">
169 <div
170 role="button"
171 tabIndex="0"
172 aria-label={sprintf(
173 __('Press to import %s', 'extendify'),
174 template?.fields?.type,
175 )}
176 style={{ maxHeight }}
177 className="button-focus relative m-0 cursor-pointer overflow-hidden bg-gray-100 ease-in-out"
178 onFocus={focusTrapInnerBlocks}
179 onClick={importTemplate}
180 onKeyDown={handleKeyDown}>
181 <div
182 ref={importButtonRef}
183 style={{ top: topValue, transitionProperty: 'all' }}
184 className={classNames('with-light-shadow relative', {
185 [`is-template--${template.fields.status}`]:
186 template?.fields?.status && devMode,
187 'p-6 md:p-8': Number.isInteger(maxHeight),
188 })}>
189 <BlockPreview
190 blocks={blocks}
191 live={false}
192 viewportWidth={1400}
193 />
194 </div>
195 </div>
196 {/* Show dev info after the preview is loaded to trigger observer */}
197 {devMode && loaded && <DevButtonOverlay template={template} />}
198 {template?.fields?.pro && (
199 <div className="pointer-events-none absolute top-4 right-4 z-20 rounded-md border border-none bg-white bg-wp-theme-500 py-1 px-2.5 font-medium text-white no-underline shadow-sm">
200 {__('Pro', 'extendify')}
201 </div>
202 )}
203 </div>
204 )
205 }
206