@bycrux/editor 0.4.6 → 0.5.0
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.
package/package.json
CHANGED
|
@@ -197,6 +197,24 @@ export default function CarouselEditor<P extends Project = Project>({ project: i
|
|
|
197
197
|
return () => window.removeEventListener('keydown', onKey)
|
|
198
198
|
}, [state])
|
|
199
199
|
|
|
200
|
+
// ── Keyboard shortcut: Delete / Backspace removes the selected element. ──
|
|
201
|
+
// Guarded against text inputs (so editing an overlay's text or a panel field
|
|
202
|
+
// never deletes the element) and against crop mode (Backspace there belongs to
|
|
203
|
+
// the crop UI). Deleting mutates project state, so the canvas + thumbnails
|
|
204
|
+
// re-render without the element immediately.
|
|
205
|
+
useEffect(() => {
|
|
206
|
+
const onKey = (e: KeyboardEvent) => {
|
|
207
|
+
if (isTypingTarget(e.target)) return
|
|
208
|
+
if (e.key !== 'Delete' && e.key !== 'Backspace') return
|
|
209
|
+
if (!selectedSlideId || !selectedElementId || cropElementId) return
|
|
210
|
+
e.preventDefault()
|
|
211
|
+
void state.removeElement(selectedSlideId, selectedElementId)
|
|
212
|
+
setSelectedElementId(null)
|
|
213
|
+
}
|
|
214
|
+
window.addEventListener('keydown', onKey)
|
|
215
|
+
return () => window.removeEventListener('keydown', onKey)
|
|
216
|
+
}, [state, selectedSlideId, selectedElementId, cropElementId])
|
|
217
|
+
|
|
200
218
|
async function handleRender() {
|
|
201
219
|
setRendering(true)
|
|
202
220
|
try {
|
|
@@ -342,11 +342,12 @@ export default function SlideCanvas({
|
|
|
342
342
|
data-testid="snap-guide-x"
|
|
343
343
|
style={{
|
|
344
344
|
position: 'absolute',
|
|
345
|
-
left: g.at * scale -
|
|
345
|
+
left: g.at * scale - 1,
|
|
346
346
|
top: 0,
|
|
347
|
-
width:
|
|
347
|
+
width: 2,
|
|
348
348
|
height: displayH,
|
|
349
|
-
background: '#
|
|
349
|
+
background: '#ff2d88',
|
|
350
|
+
boxShadow: '0 0 6px 1px rgba(255,45,136,0.9)',
|
|
350
351
|
pointerEvents: 'none',
|
|
351
352
|
zIndex: 999,
|
|
352
353
|
}}
|
|
@@ -358,10 +359,11 @@ export default function SlideCanvas({
|
|
|
358
359
|
style={{
|
|
359
360
|
position: 'absolute',
|
|
360
361
|
left: 0,
|
|
361
|
-
top: g.at * scale -
|
|
362
|
+
top: g.at * scale - 1,
|
|
362
363
|
width: displayW,
|
|
363
|
-
height:
|
|
364
|
-
background: '#
|
|
364
|
+
height: 2,
|
|
365
|
+
background: '#ff2d88',
|
|
366
|
+
boxShadow: '0 0 6px 1px rgba(255,45,136,0.9)',
|
|
365
367
|
pointerEvents: 'none',
|
|
366
368
|
zIndex: 999,
|
|
367
369
|
}}
|
|
@@ -437,7 +439,12 @@ export default function SlideCanvas({
|
|
|
437
439
|
<img
|
|
438
440
|
src={resolveSrc(element)}
|
|
439
441
|
draggable={false}
|
|
440
|
-
|
|
442
|
+
// Uncropped images use `contain` to mirror the renderer
|
|
443
|
+
// (render/templates/slide.jsx: no-crop branch → objectFit
|
|
444
|
+
// 'contain'). Logos/escudos placed in a fixed box must keep
|
|
445
|
+
// their aspect (letterboxed) in BOTH paths; `cover` here would
|
|
446
|
+
// crop them in the preview and diverge from the final PNG.
|
|
447
|
+
style={{ width: '100%', height: '100%', objectFit: 'contain', display: 'block' }}
|
|
441
448
|
alt=""
|
|
442
449
|
/>
|
|
443
450
|
)
|
|
@@ -290,4 +290,43 @@ describe('CarouselEditor — editor-core integration', () => {
|
|
|
290
290
|
expect(lastSelection()).toBeNull()
|
|
291
291
|
})
|
|
292
292
|
})
|
|
293
|
+
|
|
294
|
+
// Delete / Backspace removes the selected element and persists the removal.
|
|
295
|
+
it('deletes the selected element on Delete keypress', async () => {
|
|
296
|
+
const adapter = makeFakeAdapter()
|
|
297
|
+
const initial = makeProject()
|
|
298
|
+
render(<CarouselEditor project={initial} adapter={adapter} onProjectChange={vi.fn()} />)
|
|
299
|
+
|
|
300
|
+
const wrapper = await waitFor(() => findInteractiveWrapper('el-img'))
|
|
301
|
+
await act(async () => { fireEvent.click(wrapper) })
|
|
302
|
+
|
|
303
|
+
await act(async () => { fireEvent.keyDown(window, { key: 'Delete' }) })
|
|
304
|
+
|
|
305
|
+
// The removal is persisted: the latest saved project has no elements.
|
|
306
|
+
await waitFor(() => {
|
|
307
|
+
expect(adapter.saveCalls.length).toBeGreaterThan(0)
|
|
308
|
+
const saved = adapter.saveCalls[adapter.saveCalls.length - 1].project
|
|
309
|
+
expect(saved.slides![0].elements.find(e => e.id === 'el-img')).toBeUndefined()
|
|
310
|
+
})
|
|
311
|
+
})
|
|
312
|
+
|
|
313
|
+
// Guard: Delete/Backspace must not delete while typing (e.g. editing text or a
|
|
314
|
+
// panel field), or Backspace in a field would wipe the element.
|
|
315
|
+
it('does not delete the selected element while typing in an input', async () => {
|
|
316
|
+
const adapter = makeFakeAdapter()
|
|
317
|
+
const initial = makeProject()
|
|
318
|
+
render(<CarouselEditor project={initial} adapter={adapter} onProjectChange={vi.fn()} />)
|
|
319
|
+
|
|
320
|
+
const wrapper = await waitFor(() => findInteractiveWrapper('el-img'))
|
|
321
|
+
await act(async () => { fireEvent.click(wrapper) })
|
|
322
|
+
|
|
323
|
+
const input = document.createElement('input')
|
|
324
|
+
document.body.appendChild(input)
|
|
325
|
+
input.focus()
|
|
326
|
+
|
|
327
|
+
const before = adapter.saveCalls.length
|
|
328
|
+
await act(async () => { fireEvent.keyDown(input, { key: 'Backspace' }) })
|
|
329
|
+
expect(adapter.saveCalls.length).toBe(before)
|
|
330
|
+
document.body.removeChild(input)
|
|
331
|
+
})
|
|
293
332
|
})
|
|
@@ -61,7 +61,7 @@ describe('SlideCanvas crop display', () => {
|
|
|
61
61
|
expect(wrapper.style.overflow).toBe('hidden')
|
|
62
62
|
})
|
|
63
63
|
|
|
64
|
-
it('renders plain
|
|
64
|
+
it('renders plain contain (no crop wrapper) when element.crop is absent', async () => {
|
|
65
65
|
const el: ImageElement = {
|
|
66
66
|
id: 'el-img',
|
|
67
67
|
type: 'image',
|
|
@@ -88,7 +88,9 @@ describe('SlideCanvas crop display', () => {
|
|
|
88
88
|
if (!found) throw new Error('img not rendered')
|
|
89
89
|
return found
|
|
90
90
|
})
|
|
91
|
-
|
|
91
|
+
// Uncropped images use `contain` to mirror the renderer (no-crop branch in
|
|
92
|
+
// render/templates/slide.jsx) so logos/escudos keep aspect in both paths.
|
|
93
|
+
expect(img.style.objectFit).toBe('contain')
|
|
92
94
|
expect(img.style.width).toBe('100%')
|
|
93
95
|
expect(img.style.height).toBe('100%')
|
|
94
96
|
})
|