@blokkli/editor 1.1.0 → 1.1.2

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.
Files changed (39) hide show
  1. package/dist/module.json +1 -1
  2. package/dist/module.mjs +12 -8
  3. package/dist/runtime/components/Blocks/FromLibrary/index.vue +2 -8
  4. package/dist/runtime/components/BlokkliItem.vue +1 -0
  5. package/dist/runtime/components/Edit/Actions/index.vue +5 -0
  6. package/dist/runtime/components/Edit/AnimationCanvas/index.vue +2 -2
  7. package/dist/runtime/components/Edit/BlockProxy/index.vue +40 -9
  8. package/dist/runtime/components/Edit/DragInteractions/index.vue +16 -7
  9. package/dist/runtime/components/Edit/DraggableList.vue +3 -3
  10. package/dist/runtime/components/Edit/Features/Artboard/Overview/index.vue +7 -5
  11. package/dist/runtime/components/Edit/Features/Artboard/index.vue +7 -1
  12. package/dist/runtime/components/Edit/Features/BlockAddList/index.vue +29 -4
  13. package/dist/runtime/components/Edit/Features/Clipboard/index.vue +27 -9
  14. package/dist/runtime/components/Edit/Features/CommandPalette/Palette/index.vue +7 -1
  15. package/dist/runtime/components/Edit/Features/CommandPalette/index.vue +1 -1
  16. package/dist/runtime/components/Edit/Features/Delete/index.vue +46 -2
  17. package/dist/runtime/components/Edit/Features/DraggingOverlay/DropTargets/index.vue +2 -2
  18. package/dist/runtime/components/Edit/Features/DraggingOverlay/index.vue +14 -6
  19. package/dist/runtime/components/Edit/Features/Duplicate/index.vue +33 -15
  20. package/dist/runtime/components/Edit/Features/History/List/index.vue +149 -0
  21. package/dist/runtime/components/Edit/Features/History/index.vue +7 -134
  22. package/dist/runtime/components/Edit/Features/Library/index.vue +1 -1
  23. package/dist/runtime/components/Edit/Features/Structure/index.vue +1 -0
  24. package/dist/runtime/components/Edit/ScrollBoundary/index.vue +17 -2
  25. package/dist/runtime/composables/defineBlokkli.js +10 -3
  26. package/dist/runtime/composables/defineBlokkliFragment.js +6 -3
  27. package/dist/runtime/css/output.css +1 -1
  28. package/dist/runtime/helpers/domProvider.d.ts +9 -5
  29. package/dist/runtime/helpers/domProvider.js +33 -34
  30. package/dist/runtime/helpers/index.d.ts +2 -1
  31. package/dist/runtime/helpers/index.js +9 -0
  32. package/dist/runtime/helpers/keyboardProvider.d.ts +1 -0
  33. package/dist/runtime/helpers/keyboardProvider.js +13 -2
  34. package/dist/runtime/helpers/selectionProvider.d.ts +1 -1
  35. package/dist/runtime/helpers/selectionProvider.js +6 -0
  36. package/dist/runtime/helpers/stateProvider.d.ts +1 -0
  37. package/dist/runtime/helpers/stateProvider.js +9 -1
  38. package/dist/runtime/types/index.d.ts +6 -0
  39. package/package.json +1 -1
package/dist/module.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "blokkli",
3
3
  "configKey": "blokkli",
4
- "version": "1.1.0",
4
+ "version": "1.1.2",
5
5
  "compatibility": {
6
6
  "nuxt": "^3.12.0"
7
7
  },
package/dist/module.mjs CHANGED
@@ -7,16 +7,20 @@ import MagicString from 'magic-string';
7
7
  import { walk } from 'estree-walker-ts';
8
8
  import { BK_VISIBLE_LANGUAGES, BK_HIDDEN_GLOBALLY } from '../dist/runtime/helpers/symbols.js';
9
9
 
10
- const version = "1.1.0";
10
+ const version = "1.1.2";
11
11
 
12
12
  function sortObjectKeys(obj) {
13
- const sortedKeys = Object.keys(obj).sort();
14
- const sortedObj = {};
15
- sortedKeys.forEach((key) => {
16
- const value = obj[key];
17
- sortedObj[key] = typeof value === "object" && value !== null ? sortObjectKeys(value) : value;
18
- });
19
- return sortedObj;
13
+ if (Array.isArray(obj)) {
14
+ return obj.map(sortObjectKeys);
15
+ } else if (obj && typeof obj === "object") {
16
+ const sortedObj = {};
17
+ const keys = Object.keys(obj).sort();
18
+ for (const key of keys) {
19
+ sortedObj[key] = sortObjectKeys(obj[key]);
20
+ }
21
+ return sortedObj;
22
+ }
23
+ return obj;
20
24
  }
21
25
 
22
26
  function falsy(value) {
@@ -19,16 +19,10 @@ import {
19
19
  INJECT_IS_IN_REUSABLE,
20
20
  INJECT_REUSABLE_OPTIONS,
21
21
  } from '#blokkli/helpers/symbols'
22
- import type { FieldListItem } from '#blokkli/types'
23
-
24
- interface LibraryItem {
25
- block?: FieldListItem
26
- label?: string
27
- uuid?: string
28
- }
22
+ import type { LibraryItemProps } from '#blokkli/types'
29
23
 
30
24
  const props = defineProps<{
31
- libraryItem?: LibraryItem
25
+ libraryItem?: LibraryItemProps
32
26
  }>()
33
27
 
34
28
  const { index, options, parentType } = defineBlokkli({
@@ -72,6 +72,7 @@ const isGlobalProxyMode = inject<ComputedRef<boolean> | null>(
72
72
 
73
73
  const fieldListType = inject<ComputedRef<ValidFieldListTypes> | undefined>(
74
74
  INJECT_FIELD_LIST_TYPE,
75
+ undefined,
75
76
  )
76
77
 
77
78
  const component =
@@ -198,6 +198,11 @@ onBlokkliEvent('canvas:draw', () => {
198
198
  const rects = selection.uuids.value
199
199
  .map((uuid) => dom.getBlockRect(uuid))
200
200
  .filter(falsy)
201
+
202
+ if (!rects.length) {
203
+ return
204
+ }
205
+
201
206
  const offset = ui.artboardOffset.value
202
207
  const scale = ui.artboardScale.value
203
208
 
@@ -60,8 +60,8 @@ const scissor = computed(() => {
60
60
  canvasAttributes.value.height -
61
61
  ui.visibleViewport.value.y * dpi -
62
62
  ui.visibleViewport.value.height * dpi,
63
- width: ui.visibleViewport.value.width * dpi,
64
- height: ui.visibleViewport.value.height * dpi,
63
+ width: Math.max(ui.visibleViewport.value.width * dpi, 1),
64
+ height: Math.max(ui.visibleViewport.value.height * dpi, 1),
65
65
  }
66
66
  })
67
67
 
@@ -1,11 +1,11 @@
1
1
  <template>
2
- <div ref="root" class="bk-block-proxy">
2
+ <div ref="root" class="bk-block-proxy" v-bind="rootProps">
3
3
  <div class="bk-block-proxy-header">
4
4
  <ItemIcon :bundle="bundle" />
5
5
  {{ type?.label }}
6
6
  </div>
7
7
  <div v-if="proxyComponent" class="bk-block-proxy-component">
8
- <Component :is="proxyComponent" v-bind="itemProps" />
8
+ <Component :is="proxyComponent" v-bind="proxyComponentProps" />
9
9
  </div>
10
10
  <div v-if="fieldLayout.length" class="bk-block-proxy-fields">
11
11
  <div
@@ -41,8 +41,8 @@ import {
41
41
  } from '#blokkli/definitions'
42
42
 
43
43
  import { ItemIcon } from '#blokkli/components'
44
- import type { FieldConfig } from '#blokkli/types'
45
- import { falsy } from '#blokkli/helpers'
44
+ import type { FieldConfig, LibraryItemProps } from '#blokkli/types'
45
+ import { buildAttributesForLibraryItem, falsy } from '#blokkli/helpers'
46
46
 
47
47
  const props = defineProps<{
48
48
  uuid: string
@@ -52,16 +52,47 @@ const props = defineProps<{
52
52
  itemProps?: any
53
53
  }>()
54
54
 
55
+ // Props of the library item, if this is a 'from_library' block.
56
+ const libraryItemProps = computed<LibraryItemProps | null>(() => {
57
+ if (props.bundle === 'from_library') {
58
+ const v = props.itemProps?.libraryItem
59
+ return v as LibraryItemProps
60
+ }
61
+
62
+ return null
63
+ })
64
+
65
+ const proxyComponentProps = computed(() => {
66
+ if (props.bundle === 'from_library') {
67
+ // Pass the props of the reusable block to the proxy component.
68
+ return libraryItemProps.value?.block?.props
69
+ }
70
+
71
+ return props.itemProps
72
+ })
73
+
74
+ const rootProps = computed(() => {
75
+ if (libraryItemProps.value) {
76
+ return buildAttributesForLibraryItem(libraryItemProps.value)
77
+ }
78
+
79
+ return {}
80
+ })
81
+
82
+ const proxyBundle = computed(
83
+ () => libraryItemProps.value?.block?.bundle || props.bundle,
84
+ )
85
+
55
86
  const { dom, types, runtimeConfig } = useBlokkli()
56
87
 
57
88
  const root = ref<HTMLElement | null>(null)
58
89
 
59
- const type = computed(() => types.getBlockBundleDefinition(props.bundle))
90
+ const type = computed(() => types.getBlockBundleDefinition(proxyBundle.value))
60
91
 
61
- const proxyComponent = getBlokkliItemProxyComponent(props.bundle)
92
+ const proxyComponent = getBlokkliItemProxyComponent(proxyBundle.value)
62
93
 
63
94
  const definition = getDefinition(
64
- props.bundle,
95
+ proxyBundle.value,
65
96
  props.fieldListType,
66
97
  props.parentType,
67
98
  )
@@ -73,7 +104,7 @@ const fieldLayout = computed<FieldConfig[][]>(() => {
73
104
  .map((fieldName) => {
74
105
  return types.fieldConfig.forName(
75
106
  runtimeConfig.itemEntityType,
76
- props.bundle,
107
+ proxyBundle.value,
77
108
  fieldName,
78
109
  )
79
110
  })
@@ -82,7 +113,7 @@ const fieldLayout = computed<FieldConfig[][]>(() => {
82
113
  }
83
114
 
84
115
  return types.fieldConfig
85
- .forEntityTypeAndBundle(runtimeConfig.itemEntityType, props.bundle)
116
+ .forEntityTypeAndBundle(runtimeConfig.itemEntityType, proxyBundle.value)
86
117
  .map((config) => [config])
87
118
  })
88
119
 
@@ -183,6 +183,8 @@ function onPointerDown(e: PointerEvent) {
183
183
  e.stopPropagation()
184
184
  e.stopImmediatePropagation()
185
185
  }
186
+ // Set the state of the pressed shortcuts.
187
+ keyboard.setShortcutStateFromEvent(e)
186
188
 
187
189
  rootEl.removeEventListener('pointermove', onPointerMove)
188
190
  rootEl.addEventListener('pointermove', onPointerMove)
@@ -198,16 +200,23 @@ function onPointerDown(e: PointerEvent) {
198
200
  return
199
201
  }
200
202
 
201
- pointerDownTimestamp = Date.now()
202
203
  const coords = { x: e.clientX, y: e.clientY }
203
- mouseStartCoordinates = coords
204
-
205
- const interacted = getInteractedElement(e)
206
- pointerDownElement = interacted
207
- if (interacted) {
208
- return
204
+ // Only handle click interactions when:
205
+ // - not pressing the shift key
206
+ // - using the left mouse button
207
+ if (!e.shiftKey && e.buttons !== 2) {
208
+ pointerDownTimestamp = Date.now()
209
+ mouseStartCoordinates = coords
210
+
211
+ const interacted = getInteractedElement(e)
212
+ pointerDownElement = interacted
213
+ if (interacted) {
214
+ return
215
+ }
209
216
  }
210
217
 
218
+ // Either pressing shift or right mouse button.
219
+ // Features may handle this via event (e.g. start multi select).
211
220
  eventBus.emit('mouse:down', { ...coords, type: 'mouse', distance: 0 })
212
221
  }
213
222
 
@@ -200,18 +200,18 @@ function isMuted(item?: FieldListItem) {
200
200
 
201
201
  watch(root, function (newRoot) {
202
202
  if (newRoot) {
203
- dom.updateFieldElement(props.entity.uuid, props.name, newRoot)
203
+ dom.updateFieldElement(props.entity, props.name, newRoot)
204
204
  }
205
205
  })
206
206
 
207
207
  onMounted(() => {
208
208
  if (root.value) {
209
- dom.registerField(props.entity.uuid, props.name, root.value)
209
+ dom.registerField(props.entity, props.name, root.value)
210
210
  }
211
211
  })
212
212
 
213
213
  onBeforeUnmount(() => {
214
- dom.unregisterField(props.entity.uuid, props.name)
214
+ dom.unregisterField(props.entity, props.name)
215
215
  })
216
216
 
217
217
  defineOptions({
@@ -60,8 +60,10 @@ function updateCanvas() {
60
60
 
61
61
  const scale = rect.width / ui.artboardSize.value.width
62
62
 
63
+ const LINE_WIDTH = 2.5
64
+
63
65
  ctx.fillStyle = overviewFillColor.value
64
- ctx.lineWidth = 1
66
+ ctx.lineWidth = LINE_WIDTH
65
67
  ctx.strokeStyle = selectedColor.value
66
68
 
67
69
  for (let i = 0; i < rects.length; i++) {
@@ -74,10 +76,10 @@ function updateCanvas() {
74
76
  )
75
77
  if (selection.isBlockSelected(uuid)) {
76
78
  ctx.strokeRect(
77
- Math.round(blockRect.x * scale) + 0.5,
78
- Math.round(blockRect.y * scale) + 0.5,
79
- Math.round(blockRect.width * scale),
80
- Math.round(blockRect.height * scale),
79
+ Math.round(blockRect.x * scale) + LINE_WIDTH / 2,
80
+ Math.round(blockRect.y * scale) + LINE_WIDTH / 2,
81
+ Math.round(blockRect.width * scale) - LINE_WIDTH,
82
+ Math.round(blockRect.height * scale) - LINE_WIDTH,
81
83
  )
82
84
  }
83
85
  }
@@ -106,7 +106,7 @@ const { settings } = defineBlokkliFeature({
106
106
  screenshot: 'feature-artboard.jpg',
107
107
  })
108
108
 
109
- const { context, storage, ui, animation, $t, dom } = useBlokkli()
109
+ const { context, storage, ui, animation, $t, dom, selection } = useBlokkli()
110
110
 
111
111
  const zoomLevel = computed(() => Math.round(ui.artboardScale.value * 100) + '%')
112
112
 
@@ -169,6 +169,12 @@ watch(wheelOptions, function (newOptions) {
169
169
  }
170
170
  })
171
171
 
172
+ watch(selection.uuids, function () {
173
+ if (artboard.getMomentum()) {
174
+ artboard.cancelAnimation()
175
+ }
176
+ })
177
+
172
178
  function getArtboard(): Artboard {
173
179
  pluginWheel = wheel(wheelOptions.value)
174
180
  return createArtboard(
@@ -69,7 +69,7 @@ import { getDefaultDefinition } from '#blokkli/definitions'
69
69
  import defineCommands from '#blokkli/helpers/composables/defineCommands'
70
70
  import onBlokkliEvent from '#blokkli/helpers/composables/onBlokkliEvent'
71
71
  import { PluginTourItem } from '#blokkli/plugins'
72
- import { getFieldKey } from '#blokkli/helpers'
72
+ import { getFieldKey, onlyUnique } from '#blokkli/helpers'
73
73
 
74
74
  const { settings } = defineBlokkliFeature({
75
75
  id: 'block-add-list',
@@ -157,6 +157,31 @@ const getAllowedTypesForSelected = (p: DraggableExistingBlock): string[] => {
157
157
  }
158
158
  }
159
159
 
160
+ // All allowed bundles for which a field is being rendered currently.
161
+ // Some blocks may have nested blocks, however they may not render them via
162
+ // a <BlokkliField>. This would make it so that these nested block bundles
163
+ // show up in the add list, but there is no place where these could be added.
164
+ const bundlesForRenderedFields = computed(() =>
165
+ dom.registeredFieldTypes.value
166
+ .flatMap((field) => {
167
+ return (
168
+ types.getFieldConfig(
169
+ field.entityType,
170
+ field.entityBundle,
171
+ field.fieldName,
172
+ )?.allowedBundles || []
173
+ )
174
+ })
175
+ .filter(onlyUnique),
176
+ )
177
+
178
+ const generallyAvailableBundles = computed(() =>
179
+ types.generallyAvailableBundles.filter((v) =>
180
+ // Exclude bundles for which no field is currently being rendered.
181
+ bundlesForRenderedFields.value.includes(v.id),
182
+ ),
183
+ )
184
+
160
185
  const selectableBundles = computed(() => {
161
186
  if (selection.blocks.value.length) {
162
187
  return selection.blocks.value.flatMap((v) => getAllowedTypesForSelected(v))
@@ -174,10 +199,10 @@ const selectableBundles = computed(() => {
174
199
  )
175
200
  }
176
201
 
177
- return types.generallyAvailableBundles.map((v) => v.id || '')
202
+ return generallyAvailableBundles.value.map((v) => v.id || '')
178
203
  })
179
204
 
180
- const determineVisibility = (bundle: string, label: string): boolean => {
205
+ function determineVisibility(bundle: string, label: string): boolean {
181
206
  if (ui.isMobile.value && !selectableBundles.value.includes(bundle)) {
182
207
  return false
183
208
  }
@@ -200,7 +225,7 @@ const determineVisibility = (bundle: string, label: string): boolean => {
200
225
  }
201
226
 
202
227
  const sortedList = computed(() => {
203
- return [...types.generallyAvailableBundles]
228
+ return [...generallyAvailableBundles.value]
204
229
  .filter((v) => !reservedBundles.includes(v.id))
205
230
  .map((v) => {
206
231
  const isVisible = determineVisibility(v.id, v.label)
@@ -76,7 +76,7 @@ import {
76
76
  import { PluginSidebar } from '#blokkli/plugins'
77
77
  import ClipboardList from './List/index.vue'
78
78
  import type { ClipboardItem } from '#blokkli/types'
79
- import { falsy, generateUUID } from '#blokkli/helpers'
79
+ import { falsy, generateUUID, getFieldKey } from '#blokkli/helpers'
80
80
  import { Icon } from '#blokkli/components'
81
81
  import onBlokkliEvent from '#blokkli/helpers/composables/onBlokkliEvent'
82
82
  import defineShortcut from '#blokkli/helpers/composables/defineShortcut'
@@ -100,7 +100,7 @@ const { settings, logger } = defineBlokkliFeature({
100
100
  screenshot: 'feature-clipboard.jpg',
101
101
  })
102
102
 
103
- const { selection, $t, adapter, dom, state, ui } = useBlokkli()
103
+ const { selection, $t, adapter, dom, state, ui, types } = useBlokkli()
104
104
 
105
105
  const plugin = ref<InstanceType<typeof PluginSidebar> | null>(null)
106
106
 
@@ -271,21 +271,39 @@ const handleSelectionPaste = (pastedUuids: string[]) => {
271
271
  }
272
272
 
273
273
  // @TODO: Paste into nested field if possible.
274
- const field = dom.getBlockField(selection.uuids.value[0])
274
+ const block = selection.blocks.value[0]
275
+ if (!block) {
276
+ return
277
+ }
278
+ const field = state.getMutatedField(block.hostUuid, block.hostFieldName)
279
+ if (!field) {
280
+ return
281
+ }
282
+ const fieldConfig = types.getFieldConfig(
283
+ field.entityType,
284
+ block.hostBundle,
285
+ field.name,
286
+ )
287
+
288
+ if (!fieldConfig) {
289
+ return
290
+ }
291
+
292
+ const fieldKey = getFieldKey(field.entityUuid, field.name)
275
293
 
276
294
  const pastedBlocks = pastedUuids
277
295
  .map((uuid) => dom.findBlock(uuid))
278
296
  .filter(falsy)
279
- .filter((block) => field.allowedBundles.includes(block.itemBundle))
297
+ .filter((block) => fieldConfig.allowedBundles.includes(block.itemBundle))
280
298
 
281
299
  if (!pastedBlocks.length) {
282
300
  return
283
301
  }
284
302
 
285
- const count = state.getFieldBlockCount(field.key)
303
+ const count = state.getFieldBlockCount(fieldKey)
286
304
  if (
287
- field.cardinality !== -1 &&
288
- count + pastedBlocks.length > field.cardinality
305
+ fieldConfig.cardinality !== -1 &&
306
+ count + pastedBlocks.length > fieldConfig.cardinality
289
307
  ) {
290
308
  return
291
309
  }
@@ -294,8 +312,8 @@ const handleSelectionPaste = (pastedUuids: string[]) => {
294
312
  adapter.pasteExistingBlocks!({
295
313
  uuids: pastedBlocks.map((v) => v.uuid),
296
314
  host: {
297
- type: field.hostEntityType,
298
- uuid: field.hostEntityUuid,
315
+ type: field.entityType,
316
+ uuid: field.entityUuid,
299
317
  fieldName: field.name,
300
318
  },
301
319
  preceedingUuid: selection.uuids.value[0],
@@ -1,7 +1,7 @@
1
1
  <template>
2
2
  <ScrollBoundary
3
3
  class="bk-command-palette bk-control"
4
- @keydown.stop="onKeyDown"
4
+ @keydown="onKeyDown"
5
5
  @keyup.stop
6
6
  @click.stop
7
7
  >
@@ -238,6 +238,12 @@ const onSelect = (id: string) => {
238
238
  }
239
239
 
240
240
  const onKeyDown = (e: KeyboardEvent) => {
241
+ e.stopPropagation()
242
+ if (e.code === 'KeyK' && (e.ctrlKey || e.metaKey)) {
243
+ e.preventDefault()
244
+ emit('close')
245
+ return
246
+ }
241
247
  if (e.code === 'Tab') {
242
248
  e.preventDefault()
243
249
  if (e.shiftKey) {
@@ -19,7 +19,7 @@
19
19
  )
20
20
  "
21
21
  icon="command"
22
- @click="isVisible = true"
22
+ @click="isVisible = !isVisible"
23
23
  />
24
24
  </template>
25
25
 
@@ -17,7 +17,7 @@ import { useBlokkli, defineBlokkliFeature } from '#imports'
17
17
  import type { DraggableExistingBlock } from '#blokkli/types'
18
18
  import { PluginItemAction } from '#blokkli/plugins'
19
19
 
20
- const { state, $t, eventBus } = useBlokkli()
20
+ const { state, $t, eventBus, dom, runtimeConfig } = useBlokkli()
21
21
 
22
22
  const { adapter } = defineBlokkliFeature({
23
23
  id: 'delete',
@@ -27,12 +27,56 @@ const { adapter } = defineBlokkliFeature({
27
27
  description: 'Provides an action to delete one or more blocks.',
28
28
  })
29
29
 
30
+ /**
31
+ * Try to find a block to select after deleting a single block.
32
+ */
33
+ function getSelectionAfterDelete(
34
+ items: DraggableExistingBlock[],
35
+ ): string | undefined {
36
+ if (items.length !== 1) {
37
+ return
38
+ }
39
+
40
+ const uuid = items[0].uuid
41
+ const field = state.getFieldListForBlock(uuid)
42
+ if (!field) {
43
+ return
44
+ }
45
+
46
+ const index = field.list.findIndex((v) => v.uuid === uuid)
47
+ if (index === -1) {
48
+ return
49
+ }
50
+
51
+ // Find a matching block to select in the same field list.
52
+ const inList = field.list[index + 1]?.uuid || field.list[index - 1]?.uuid
53
+ if (inList) {
54
+ return inList
55
+ }
56
+
57
+ // Field does not belong to a block.
58
+ if (field.entityType !== runtimeConfig.itemEntityType) {
59
+ return
60
+ }
61
+
62
+ // Select the block which contains the field.
63
+ return field.entityUuid
64
+ }
65
+
30
66
  async function onClick(items: DraggableExistingBlock[]) {
31
- eventBus.emit('select', [])
67
+ const selectedUuidsAfter = getSelectionAfterDelete(items)
68
+
32
69
  await state.mutateWithLoadingState(
33
70
  () => adapter.deleteBlocks(items.map((v) => v.uuid)),
34
71
  $t('deleteError', 'The block could not be deleted.'),
35
72
  )
73
+
74
+ if (selectedUuidsAfter) {
75
+ eventBus.emit('select', selectedUuidsAfter)
76
+ dom.refreshBlockRect(selectedUuidsAfter)
77
+ } else {
78
+ eventBus.emit('select:unselect')
79
+ }
36
80
  }
37
81
  </script>
38
82
 
@@ -178,9 +178,9 @@ onBlokkliEvent('mouse:up', (e) => {
178
178
 
179
179
  const emitDrop = async () => {
180
180
  const timeDelta = Date.now() - dragStart
181
- // Prevent accidental drops. At least 400ms should have passed between the
181
+ // Prevent accidental drops. At least 200ms should have passed between the
182
182
  // time the drag was initiated and when the drop was made.
183
- if (active.value && timeDelta > 400) {
183
+ if (active.value && timeDelta > 200) {
184
184
  if (active.value.type === 'field') {
185
185
  const [hostUuid, fieldName, preceedingUuid] = active.value.id.split(':')
186
186
 
@@ -169,6 +169,11 @@ const onDropExisting = async (
169
169
  host,
170
170
  }),
171
171
  )
172
+ if (uuids.length >= 1 && uuids.length <= 10) {
173
+ for (let i = 0; i < uuids.length; i++) {
174
+ dom.refreshBlockRect(uuids[i])
175
+ }
176
+ }
172
177
 
173
178
  if (ui.isMobile.value) {
174
179
  eventBus.emit('scrollIntoView', {
@@ -365,8 +370,15 @@ onBlokkliEvent('dragging:start', (e) => {
365
370
  if (!item) {
366
371
  return
367
372
  }
373
+
374
+ mouseX.value = e.coords.x
375
+ mouseY.value = e.coords.y
376
+
377
+ // Before showing the drop targets we update all currently visible rects to
378
+ // ensure the user sees the correct drop targets right away.
379
+ dom.updateVisibleRects()
380
+ dragItems.value = e.items
368
381
  if ('element' in item) {
369
- eventBus.on('animationFrame', loop)
370
382
  if (!isTouching.value) {
371
383
  document.removeEventListener('pointerup', onMouseUp)
372
384
  document.addEventListener('pointerup', onMouseUp)
@@ -375,12 +387,8 @@ onBlokkliEvent('dragging:start', (e) => {
375
387
  })
376
388
  document.addEventListener('pointermove', onMouseMove, { capture: true })
377
389
  }
390
+ eventBus.on('animationFrame', loop)
378
391
  }
379
-
380
- // Before showing the drop targets we update all currently visible rects to
381
- // ensure the user sees the correct drop targets right away.
382
- dom.updateVisibleRects()
383
- dragItems.value = e.items
384
392
  })
385
393
 
386
394
  onBlokkliEvent('dragging:end', () => {
@@ -14,14 +14,11 @@
14
14
 
15
15
  <script lang="ts" setup>
16
16
  import { computed, useBlokkli, defineBlokkliFeature } from '#imports'
17
-
18
- import type {
19
- BlokkliFieldElement,
20
- DraggableExistingBlock,
21
- } from '#blokkli/types'
17
+ import type { DraggableExistingBlock } from '#blokkli/types'
22
18
  import { PluginItemAction } from '#blokkli/plugins'
19
+ import { getFieldKey } from '#blokkli/helpers'
23
20
 
24
- const { state, $t, selection, dom } = useBlokkli()
21
+ const { state, $t, selection, types } = useBlokkli()
25
22
 
26
23
  const { adapter } = defineBlokkliFeature({
27
24
  id: 'duplicate',
@@ -44,31 +41,52 @@ const canDuplicate = computed<boolean>(() => {
44
41
  }
45
42
 
46
43
  const blocksByField: Record<string, DraggableExistingBlock[]> = {}
47
- const fieldsByKey: Record<string, BlokkliFieldElement> = {}
44
+ const fieldsByKey: Record<
45
+ string,
46
+ { cardinality: number; allowedBundles: string[]; count: number }
47
+ > = {}
48
48
 
49
49
  const selectedCount = selection.blocks.value.length
50
50
  for (let i = 0; i < selectedCount; i++) {
51
51
  const block = selection.blocks.value[i]
52
- const field = dom.getBlockField(block.uuid)
53
- const count = state.getFieldBlockCount(field.key)
52
+ const field = state.getMutatedField(block.hostUuid, block.hostFieldName)
53
+ if (!field) {
54
+ continue
55
+ }
56
+
57
+ const fieldKey = getFieldKey(field.entityUuid, field.name)
58
+
59
+ const fieldConfig = types.getFieldConfig(
60
+ field.entityType,
61
+ block.hostBundle,
62
+ field.name,
63
+ )
64
+ if (!fieldConfig) {
65
+ continue
66
+ }
67
+ const count = field.list.length
54
68
 
55
69
  // Early return if the field is already full.
56
- if (field.cardinality !== -1 && count >= field.cardinality) {
70
+ if (fieldConfig.cardinality !== -1 && count >= fieldConfig.cardinality) {
57
71
  return false
58
72
  }
59
73
 
60
- if (!blocksByField[field.key]) {
61
- blocksByField[field.key] = []
74
+ if (!blocksByField[fieldKey]) {
75
+ blocksByField[fieldKey] = []
76
+ }
77
+ blocksByField[fieldKey].push(block)
78
+ fieldsByKey[fieldKey] = {
79
+ cardinality: fieldConfig.cardinality,
80
+ allowedBundles: fieldConfig.allowedBundles,
81
+ count,
62
82
  }
63
- blocksByField[field.key].push(block)
64
- fieldsByKey[field.key] = field
65
83
  }
66
84
 
67
85
  const entries = Object.entries(blocksByField)
68
86
  for (let i = 0; i < entries.length; i++) {
69
87
  const [fieldKey, blocks] = entries[i]
70
88
  const field = fieldsByKey[fieldKey]
71
- const count = state.getFieldBlockCount(field.key)
89
+ const count = state.getFieldBlockCount(fieldKey)
72
90
  // Check cardinality of the field.
73
91
  if (field.cardinality !== -1 && count + blocks.length > field.cardinality) {
74
92
  return false