jvs-ppt-ui 0.0.11 → 0.0.14

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.
@@ -4,7 +4,7 @@ import { getFontVariantStyle } from '../../../configs/fonts'
4
4
  import { useSlides } from '../../../composables/useSlides'
5
5
  import { createTextSelectionOverlay, type TextSelectionOverlayHandle } from '../textSelectionOverlay'
6
6
 
7
- const { shapeFormatPainter } = useSlides()
7
+ const { shapeFormatPainter, isReadonly } = useSlides()
8
8
  const isTextPaintActive = computed(() => !!(shapeFormatPainter.value && shapeFormatPainter.value.mode === 'text-range'))
9
9
  const isTextSelectable = computed(() => (props.isEditingText && props.element.id === props.selectedElementId) || isTextPaintActive.value)
10
10
 
@@ -176,7 +176,7 @@ function handleMaskMouseDown(e: MouseEvent) {
176
176
  >
177
177
  <!-- Sibling Table Edit Mask -->
178
178
  <div
179
- v-if="element.id === selectedElementId && isTable(element) && !isEditingText"
179
+ v-if="!isReadonly && element.id === selectedElementId && isTable(element) && !isEditingText"
180
180
  class="table-edit-mask"
181
181
  @mousemove="handleMaskMouseMove"
182
182
  @mousedown="handleMaskMouseDown"
@@ -187,7 +187,7 @@ function handleMaskMouseDown(e: MouseEvent) {
187
187
 
188
188
  <!-- Sibling Formula Edit Mask -->
189
189
  <div
190
- v-if="element.id === selectedElementId && element.latex && !isEditingText"
190
+ v-if="!isReadonly && element.id === selectedElementId && element.latex && !isEditingText"
191
191
  class="formula-edit-mask"
192
192
  @dblclick.stop="emit('formulaDblClick')"
193
193
  ></div>
@@ -203,7 +203,7 @@ function handleMaskMouseDown(e: MouseEvent) {
203
203
  @focusout="emit('focusout', $event)"
204
204
  @input="emit('input', $event)"
205
205
  @keydown="emit('keydown', $event)"
206
- @dblclick.stop="isTextSelectable ? null : emit('dblclick', $event)"
206
+ @dblclick.stop="isReadonly ? null : (isTextSelectable ? null : emit('dblclick', $event))"
207
207
  @mousemove="emit('mousemove', $event)"
208
208
  @mouseleave="emit('mouseleave', $event)"
209
209
  :class="{
@@ -36,6 +36,7 @@ export interface ShortcutsOptions {
36
36
  shapeFormatPainter?: Ref<any>
37
37
  notify: (msg: string) => void
38
38
  addHistorySnapshot?: () => void
39
+ isReadonly?: Ref<boolean>
39
40
  }
40
41
 
41
42
  export function useCanvasShortcuts(options: ShortcutsOptions) {
@@ -73,7 +74,8 @@ export function useCanvasShortcuts(options: ShortcutsOptions) {
73
74
  getElementPixels,
74
75
  shapeFormatPainter,
75
76
  notify,
76
- addHistorySnapshot
77
+ addHistorySnapshot,
78
+ isReadonly
77
79
  } = options
78
80
 
79
81
  let historyDebounceTimer: any = null
@@ -89,6 +91,53 @@ export function useCanvasShortcuts(options: ShortcutsOptions) {
89
91
  function handleGlobalKeyDown(e: KeyboardEvent) {
90
92
  if (e.defaultPrevented) return
91
93
 
94
+ if (isReadonly?.value) {
95
+ if (e.key === 'F5') {
96
+ e.preventDefault()
97
+ if (!isPresenting.value) {
98
+ if (e.shiftKey) {
99
+ startPresentation()
100
+ } else {
101
+ activeIndex.value = 0
102
+ startPresentation()
103
+ }
104
+ }
105
+ return
106
+ }
107
+ if ((e.ctrlKey || e.metaKey) && e.key.toLowerCase() === 'f') {
108
+ showSearchPanel.value = !showSearchPanel.value
109
+ if (showSearchPanel.value) {
110
+ activeSearchTab.value = 'find'
111
+ }
112
+ e.preventDefault()
113
+ return
114
+ }
115
+ if (e.key === 'Escape') {
116
+ selectedElementId.value = null
117
+ selectedElementIds.value = []
118
+ return
119
+ }
120
+ if (e.key === 'ArrowUp' || e.key === 'PageUp') {
121
+ if (activeIndex.value > 0) activeIndex.value--
122
+ e.preventDefault()
123
+ return
124
+ }
125
+ if (e.key === 'ArrowDown' || e.key === 'PageDown') {
126
+ if (activeIndex.value < slides.value.length - 1) activeIndex.value++
127
+ e.preventDefault()
128
+ return
129
+ }
130
+ if ((e.ctrlKey || e.metaKey) && (e.key === 'c' || e.key === 'C')) {
131
+ copySelectedSlides()
132
+ return
133
+ }
134
+ if (['Delete', 'Backspace'].includes(e.key) || ((e.ctrlKey || e.metaKey) && ['d', 'g', 'z', 'y', 'v', 'x'].includes(e.key.toLowerCase()))) {
135
+ e.preventDefault()
136
+ e.stopPropagation()
137
+ }
138
+ return
139
+ }
140
+
92
141
  // Ctrl + D (Duplicate / 快速复制副本) - 拦截防止浏览器添加书签
93
142
  if ((e.ctrlKey || e.metaKey) && (e.key === 'd' || e.key === 'D')) {
94
143
  e.preventDefault()
@@ -12,6 +12,7 @@ export interface TableOptions {
12
12
  getOverlayHeight?: (el: any) => string
13
13
  selectedElementWidth?: Ref<string>
14
14
  selectedElementHeight?: Ref<string>
15
+ isReadonly?: Ref<boolean>
15
16
  }
16
17
 
17
18
  interface TableMeasure {
@@ -57,7 +58,8 @@ export function useCanvasTable(options: TableOptions) {
57
58
  getOverlayWidth = (el: any) => el?.style?.width || '500px',
58
59
  getOverlayHeight = (el: any) => el?.style?.height || '200px',
59
60
  selectedElementWidth,
60
- selectedElementHeight
61
+ selectedElementHeight,
62
+ isReadonly
61
63
  } = options
62
64
 
63
65
  const selectedCellCount = ref(0)
@@ -461,7 +463,7 @@ export function useCanvasTable(options: TableOptions) {
461
463
  }
462
464
 
463
465
  function selectColumnCells(el: any, colIdx: number) {
464
- if (!el || !el.id) return
466
+ if (isReadonly?.value || !el || !el.id) return
465
467
  selectedElementId.value = el.id
466
468
  if (!selectedElementIds.value.includes(el.id)) {
467
469
  selectedElementIds.value = [el.id]
@@ -508,7 +510,7 @@ export function useCanvasTable(options: TableOptions) {
508
510
  }
509
511
 
510
512
  function selectRowCells(el: any, rowIdx: number) {
511
- if (!el || !el.id) return
513
+ if (isReadonly?.value || !el || !el.id) return
512
514
  selectedElementId.value = el.id
513
515
  if (!selectedElementIds.value.includes(el.id)) {
514
516
  selectedElementIds.value = [el.id]
@@ -1297,6 +1299,7 @@ export function useCanvasTable(options: TableOptions) {
1297
1299
  }
1298
1300
 
1299
1301
  function startTableColResize(e: MouseEvent, el: any, targetColIdx: number) {
1302
+ if (isReadonly?.value) return
1300
1303
  e.preventDefault()
1301
1304
  e.stopPropagation()
1302
1305
 
@@ -1401,6 +1404,7 @@ export function useCanvasTable(options: TableOptions) {
1401
1404
  }
1402
1405
 
1403
1406
  function startTableRowResize(e: MouseEvent, el: any, targetRowIdx: number) {
1407
+ if (isReadonly?.value) return
1404
1408
  e.preventDefault()
1405
1409
  e.stopPropagation()
1406
1410
 
@@ -1613,6 +1617,7 @@ export function useCanvasTable(options: TableOptions) {
1613
1617
  }
1614
1618
 
1615
1619
  function handleTableDblClick(e: MouseEvent, el: any) {
1620
+ if (isReadonly?.value) return
1616
1621
  e.stopPropagation()
1617
1622
  const targetEl = el || selectedElement.value
1618
1623
  if (!targetEl) return
@@ -25,6 +25,7 @@ export interface TextOptions {
25
25
  renderLatexToSvg: (latex: string) => string
26
26
  sanitizeNbspInHtml: (html: string) => string
27
27
  updateTextFontScale: (el: any) => void
28
+ isReadonly?: Ref<boolean>
28
29
  }
29
30
 
30
31
  export function useCanvasText(options: TextOptions) {
@@ -48,7 +49,8 @@ export function useCanvasText(options: TextOptions) {
48
49
  applyTextStyleToHtml,
49
50
  renderLatexToSvg,
50
51
  sanitizeNbspInHtml,
51
- updateTextFontScale
52
+ updateTextFontScale,
53
+ isReadonly
52
54
  } = options
53
55
 
54
56
  const isEditingText = ref(false)
@@ -56,7 +58,7 @@ export function useCanvasText(options: TextOptions) {
56
58
  const editingPlaceholderOriginalValue = ref<{ [key: string]: string }>({})
57
59
 
58
60
  function updateElementContent(id: string, text: string) {
59
- if (!activeSlide.value) return
61
+ if (isReadonly?.value || !activeSlide.value) return
60
62
  const el = activeSlide.value.elements.find((item: any) => item.id === id)
61
63
  if (el) {
62
64
  if (el.type === 'shape') {
@@ -198,6 +200,7 @@ export function useCanvasText(options: TextOptions) {
198
200
  }
199
201
 
200
202
  function startEditingShapeText(elOrId: any, mouseEvent?: MouseEvent) {
203
+ if (isReadonly?.value) return
201
204
  const elId = typeof elOrId === 'string' ? elOrId : elOrId?.id
202
205
  const el = typeof elOrId === 'string'
203
206
  ? (findElementInSlide(elOrId) || activeSlide.value?.elements.find((x: any) => x.id === elOrId))
@@ -255,6 +258,7 @@ export function useCanvasText(options: TextOptions) {
255
258
  }
256
259
 
257
260
  function startEditingTextElement(elId: string, mouseEvent?: MouseEvent) {
261
+ if (isReadonly?.value || !elId) return
258
262
  selectedElementId.value = elId
259
263
  selectedElementIds.value = [elId]
260
264
  isEditingText.value = true
@@ -406,7 +410,7 @@ export function useCanvasText(options: TextOptions) {
406
410
  }
407
411
 
408
412
  function handleFormulaDblClick(el: any) {
409
- if (!el.latex) return
413
+ if (isReadonly?.value || !el.latex) return
410
414
  currentEditingFormulaEl.value = el
411
415
  formulaDialogVisible.value = true
412
416
  }
@@ -1,6 +1,7 @@
1
1
  import { computed, type Ref, type ComputedRef } from 'vue'
2
2
  import { shapeFormulas } from '../../utils/pptxShapesLazy'
3
3
  import { useSlides } from '../useSlides'
4
+ import { applyGroupGeometryFlip } from '../../utils/groupFlip'
4
5
 
5
6
  export function useSidebarElementTransform(
6
7
  selectedElement: ComputedRef<any>,
@@ -65,10 +66,16 @@ export function useSidebarElementTransform(
65
66
  if (selectedElementIds.value.length > 1 && activeSlide.value) {
66
67
  activeSlide.value.elements.forEach((el: any) => {
67
68
  if (selectedElementIds.value.includes(el.id)) {
68
- if (!el.style) el.style = {}
69
- el.style.flipH = !el.style.flipH
69
+ if (el.type === 'group') {
70
+ applyGroupGeometryFlip(el, true, false)
71
+ } else {
72
+ if (!el.style) el.style = {}
73
+ el.style.flipH = !el.style.flipH
74
+ }
70
75
  }
71
76
  })
77
+ } else if (selectedElement.value && selectedElement.value.type === 'group') {
78
+ applyGroupGeometryFlip(selectedElement.value, true, false)
72
79
  } else {
73
80
  getStyle().flipH = !getStyle().flipH
74
81
  }
@@ -79,10 +86,16 @@ export function useSidebarElementTransform(
79
86
  if (selectedElementIds.value.length > 1 && activeSlide.value) {
80
87
  activeSlide.value.elements.forEach((el: any) => {
81
88
  if (selectedElementIds.value.includes(el.id)) {
82
- if (!el.style) el.style = {}
83
- el.style.flipV = !el.style.flipV
89
+ if (el.type === 'group') {
90
+ applyGroupGeometryFlip(el, false, true)
91
+ } else {
92
+ if (!el.style) el.style = {}
93
+ el.style.flipV = !el.style.flipV
94
+ }
84
95
  }
85
96
  })
97
+ } else if (selectedElement.value && selectedElement.value.type === 'group') {
98
+ applyGroupGeometryFlip(selectedElement.value, false, true)
86
99
  } else {
87
100
  getStyle().flipV = !getStyle().flipV
88
101
  }
@@ -12,6 +12,7 @@ import {
12
12
  inferElementTextType,
13
13
  normalizeTextType
14
14
  } from '../utils/textTypes'
15
+ import { applyGroupGeometryFlip } from '../utils/groupFlip'
15
16
  import type { CustomUploadHandler, UploadResourceContext, UploadResourceType } from '../types'
16
17
 
17
18
  export type { SlideTextType }
@@ -340,15 +341,15 @@ function setAiConfig(config?: { apiUrl?: string; headers?: Record<string, string
340
341
  }
341
342
  }
342
343
 
343
- // 素材库配置状态(支持获取分类树/首页、分页查询、获取单独类型、素材保存 API)
344
- export const DEFAULT_MATERIAL_API_URL = '/jvs-dam/api/dam/material/tree'
345
- export const DEFAULT_MATERIAL_PAGE_API_URL = '/jvs-dam/api/dam/material/page'
344
+ // 素材库配置状态(公网免版权素材检索 stock/search、单页模板、素材保存 API)
345
+ export const DEFAULT_MATERIAL_API_URL = '/jvs-dam/stock/search'
346
+ export const DEFAULT_MATERIAL_PAGE_API_URL = '/jvs-dam/stock/search'
346
347
 
347
348
  const defaultEnvMaterialUrl = (typeof import.meta !== 'undefined' && (import.meta as any).env)
348
- ? String((import.meta as any).env?.VITE_MATERIAL_TREE_API_URL || (import.meta as any).env?.VITE_MATERIAL_API_URL || (import.meta as any).env?.VITE_MATERIAL_LIST_API_URL || DEFAULT_MATERIAL_API_URL)
349
+ ? String((import.meta as any).env?.VITE_MATERIAL_STOCK_API_URL || (import.meta as any).env?.VITE_MATERIAL_API_URL || (import.meta as any).env?.VITE_MATERIAL_LIST_API_URL || (import.meta as any).env?.VITE_MATERIAL_TREE_API_URL || DEFAULT_MATERIAL_API_URL)
349
350
  : DEFAULT_MATERIAL_API_URL
350
351
  const defaultEnvMaterialPageUrl = (typeof import.meta !== 'undefined' && (import.meta as any).env)
351
- ? String((import.meta as any).env?.VITE_MATERIAL_PAGE_API_URL || DEFAULT_MATERIAL_PAGE_API_URL)
352
+ ? String((import.meta as any).env?.VITE_MATERIAL_PAGE_API_URL || (import.meta as any).env?.VITE_MATERIAL_STOCK_API_URL || DEFAULT_MATERIAL_PAGE_API_URL)
352
353
  : DEFAULT_MATERIAL_PAGE_API_URL
353
354
  const defaultEnvMaterialTypeUrl = (typeof import.meta !== 'undefined' && (import.meta as any).env?.VITE_MATERIAL_TYPE_API_URL)
354
355
  ? String((import.meta as any).env.VITE_MATERIAL_TYPE_API_URL)
@@ -357,11 +358,11 @@ const defaultEnvMaterialSaveUrl = (typeof import.meta !== 'undefined' && (import
357
358
  ? String((import.meta as any).env.VITE_MATERIAL_SAVE_API_URL)
358
359
  : ''
359
360
 
360
- /** 素材库获取所有内容/首页分类树的 API 地址 */
361
+ /** 素材库检索公网素材/所有内容的 API 地址 */
361
362
  const materialApiUrl = ref<string>(defaultEnvMaterialUrl)
362
- /** 素材库首页分类树 API 地址 */
363
+ /** 素材库首页/分类检索 API 地址 */
363
364
  const materialTreeApiUrl = ref<string>(defaultEnvMaterialUrl)
364
- /** 素材库分页查询 API 地址 */
365
+ /** 素材库分页/关键词检索 API 地址 */
365
366
  const materialPageApiUrl = ref<string>(defaultEnvMaterialPageUrl)
366
367
  /** 素材库按类型获取单独分类素材的 API 地址 */
367
368
  const materialTypeApiUrl = ref<string>(defaultEnvMaterialTypeUrl)
@@ -2497,6 +2498,7 @@ watch(formulaDialogVisible, (visible) => {
2497
2498
  }
2498
2499
  })
2499
2500
  const isPresenting = ref(false)
2501
+ const isReadonly = ref(false)
2500
2502
  const showNotification = ref(false)
2501
2503
  const notificationMsg = ref('')
2502
2504
  const shapeFormatPainter = ref<any>(null)
@@ -2958,6 +2960,7 @@ function applyHistoryEntry<T>(entryStrs: string[], currentStrs: string[], list:
2958
2960
  }
2959
2961
 
2960
2962
  function undo() {
2963
+ if (isReadonly.value) return
2961
2964
  if (isMasterEditingView.value) {
2962
2965
  // 先落盘防抖中的快照:编辑后 350ms 内撤销不再丢最后一步编辑
2963
2966
  flushPendingMasterHistorySnapshot()
@@ -3009,6 +3012,7 @@ function undo() {
3009
3012
  }
3010
3013
 
3011
3014
  function redo() {
3015
+ if (isReadonly.value) return
3012
3016
  if (isMasterEditingView.value) {
3013
3017
  flushPendingMasterHistorySnapshot()
3014
3018
  if (masterHistoryIndex.value < masterHistoryStack.value.length - 1) {
@@ -3183,6 +3187,7 @@ function escapeRegExp(string: string) {
3183
3187
  }
3184
3188
 
3185
3189
  function addSlide() {
3190
+ if (isReadonly.value) return
3186
3191
  const newId = Date.now().toString()
3187
3192
  const insertIndex = slides.value.length > 0 ? activeIndex.value + 1 : 0
3188
3193
  const curSlide = slides.value[activeIndex.value]
@@ -3202,6 +3207,7 @@ function addSlide() {
3202
3207
  }
3203
3208
 
3204
3209
  function deleteSlide(index: number) {
3210
+ if (isReadonly.value) return
3205
3211
  const isLastSlide = slides.value.length === 1
3206
3212
  if (isLastSlide) {
3207
3213
  const newSlide = {
@@ -3232,7 +3238,7 @@ function deleteSlide(index: number) {
3232
3238
  }
3233
3239
 
3234
3240
  function pasteSlideData(targetSlideData: any, afterIndex = activeIndex.value, isCut = false) {
3235
- if (!targetSlideData) return
3241
+ if (isReadonly.value || !targetSlideData) return
3236
3242
 
3237
3243
  if (Array.isArray(targetSlideData)) {
3238
3244
  if (targetSlideData.length === 0) return
@@ -3445,7 +3451,7 @@ function scaleElementIfNeeded(el: any, sX: number, sY: number) {
3445
3451
  }
3446
3452
 
3447
3453
  function pasteElementsData(elementsArray: any[], options?: { sourceWidth?: number; sourceHeight?: number }) {
3448
- if (!activeSlide.value || !elementsArray || !Array.isArray(elementsArray) || elementsArray.length === 0) return
3454
+ if (isReadonly.value || !activeSlide.value || !elementsArray || !Array.isArray(elementsArray) || elementsArray.length === 0) return
3449
3455
  const pastedIds: string[] = []
3450
3456
  const oldIdToNewId = new Map<string, string>()
3451
3457
  const oldGroupIdToNewGroupId = new Map<string, string>()
@@ -3889,7 +3895,7 @@ function selectAllSlides() {
3889
3895
  }
3890
3896
 
3891
3897
  function deleteSelectedSlides() {
3892
- if (!slides.value || slides.value.length === 0) return
3898
+ if (isReadonly.value || !slides.value || slides.value.length === 0) return
3893
3899
  const sortedIndexes = [...selectedSlideIndexes.value].filter(i => i >= 0 && i < slides.value.length).sort((a, b) => b - a)
3894
3900
  if (sortedIndexes.length === 0) {
3895
3901
  deleteSlide(activeIndex.value)
@@ -3942,12 +3948,14 @@ function copySelectedSlides() {
3942
3948
  }
3943
3949
 
3944
3950
  function cutSelectedSlides() {
3951
+ if (isReadonly.value) return
3945
3952
  copySelectedSlides()
3946
3953
  deleteSelectedSlides()
3947
3954
  notify('已剪切选中的幻灯片')
3948
3955
  }
3949
3956
 
3950
3957
  function duplicateSelectedSlides() {
3958
+ if (isReadonly.value) return
3951
3959
  const indexes = [...selectedSlideIndexes.value].filter(i => i >= 0 && i < slides.value.length).sort((a, b) => a - b)
3952
3960
  if (indexes.length === 0) indexes.push(activeIndex.value)
3953
3961
  const lastIndex = indexes[indexes.length - 1]
@@ -3976,6 +3984,7 @@ function duplicateSelectedSlides() {
3976
3984
  }
3977
3985
 
3978
3986
  function applyBackground(bg: string) {
3987
+ if (isReadonly.value) return
3979
3988
  if (activeSlide.value) {
3980
3989
  activeSlide.value.background = bg
3981
3990
  activeSlide.value.bgType = 'gradient'
@@ -4009,7 +4018,7 @@ function addElement(
4009
4018
  initialDimensions?: { width?: number; height?: number; left?: number; top?: number },
4010
4019
  extraOptions?: { textType?: SlideTextType; name?: string; [key: string]: any }
4011
4020
  ) {
4012
- if (!activeSlide.value) return
4021
+ if (isReadonly.value || !activeSlide.value) return
4013
4022
 
4014
4023
  const recipe = getActiveSlideThemeRecipe(activeSlide.value)
4015
4024
 
@@ -4824,7 +4833,7 @@ function updateGroupBounds(groupEl: SlideElement) {
4824
4833
 
4825
4834
  // Delete Element from current slide
4826
4835
  function deleteElement(id: string, silent = false) {
4827
- if (!activeSlide.value) return
4836
+ if (isReadonly.value || !activeSlide.value) return
4828
4837
  const hasTopLevel = activeSlide.value.elements.some(item => item.id === id)
4829
4838
  if (hasTopLevel) {
4830
4839
  activeSlide.value.elements = activeSlide.value.elements.filter(item => item.id !== id)
@@ -6582,7 +6591,7 @@ function convertPPTistToLocalIfNeed(newSlides: any): Slide[] {
6582
6591
  const grpFlipH = !!(el.flipH || el.isFlipH || el.style?.flipH)
6583
6592
  const grpFlipV = !!(el.flipV || el.isFlipV || el.style?.flipV)
6584
6593
 
6585
- return {
6594
+ const groupObj: any = {
6586
6595
  id: el.id || 'group_' + Date.now().toString(36) + Math.random().toString(36).substring(2, 7),
6587
6596
  type: 'group',
6588
6597
  name: el.name || '组合',
@@ -6601,6 +6610,12 @@ function convertPPTistToLocalIfNeed(newSlides: any): Slide[] {
6601
6610
  isFlipV: grpFlipV,
6602
6611
  elements: el.elements.map((child: any) => convertChildRecursive(child, topW, topH))
6603
6612
  }
6613
+
6614
+ if (grpFlipH || grpFlipV) {
6615
+ applyGroupGeometryFlip(groupObj, grpFlipH, grpFlipV)
6616
+ }
6617
+
6618
+ return groupObj
6604
6619
  }
6605
6620
 
6606
6621
  const elemPattern = extractPatternFromFill(el)
@@ -7283,6 +7298,9 @@ function preprocessImportedSlides(slides: any[]) {
7283
7298
  }
7284
7299
  processEl(child, grpW, grpH)
7285
7300
  })
7301
+ if (el.flipH || el.isFlipH || el.style?.flipH || el.flipV || el.isFlipV || el.style?.flipV) {
7302
+ applyGroupGeometryFlip(el)
7303
+ }
7286
7304
  }
7287
7305
  }
7288
7306
  for (const el of slide.elements) {
@@ -9255,6 +9273,7 @@ export function useSlides() {
9255
9273
  selectedElementIds,
9256
9274
  croppingElementId,
9257
9275
  isPresenting,
9276
+ isReadonly,
9258
9277
  showNotification,
9259
9278
  notificationMsg,
9260
9279
  notify,
@@ -0,0 +1,108 @@
1
+ /**
2
+ * 组合 (Group) 翻转与几何镜像计算工具函数
3
+ *
4
+ * 遵循微软 PowerPoint / DrawingML 规范标准:
5
+ * 当对组合执行水平/垂直翻转时:
6
+ * 1. 组合内部子元素的相对几何位置镜像对调(left / top 沿中心线镜像);
7
+ * 2. 子元素若是形状/图片,其自身的翻转状态联动反转;
8
+ * 3. 子元素若是纯文本(Text),文字字形永远保持正向可读(Text is never mirrored),禁止倒影;
9
+ * 4. 几何镜像处理完成后,Group 容器自身不再需要使用 CSS scale(-1) 进行整体物理镜像。
10
+ */
11
+
12
+ export function applyGroupGeometryFlip(group: any, forceFlipH?: boolean, forceFlipV?: boolean) {
13
+ if (!group || !Array.isArray(group.elements) || group.elements.length === 0) return
14
+
15
+ const flipH = forceFlipH !== undefined ? forceFlipH : Boolean(group.flipH || group.isFlipH || group.style?.flipH)
16
+ const flipV = forceFlipV !== undefined ? forceFlipV : Boolean(group.flipV || group.isFlipV || group.style?.flipV)
17
+
18
+ if (!flipH && !flipV) return
19
+
20
+ const grpW = typeof group.width === 'number' && group.width > 0 ? group.width : (parseFloat(group.style?.width) || 0)
21
+ const grpH = typeof group.height === 'number' && group.height > 0 ? group.height : (parseFloat(group.style?.height) || 0)
22
+
23
+ for (const child of group.elements) {
24
+ if (!child) continue
25
+
26
+ // 1. 水平翻转:镜像相对水平位置
27
+ if (flipH) {
28
+ if (typeof child.style?.left === 'string' && child.style.left.endsWith('%')) {
29
+ const lPct = parseFloat(child.style.left) || 0
30
+ const wPct = parseFloat(child.style.width) || 0
31
+ child.style.left = `${+(100 - lPct - wPct).toFixed(4)}%`
32
+ } else if (typeof child.left === 'number' && grpW > 0) {
33
+ const cW = typeof child.width === 'number' ? child.width : (parseFloat(child.style?.width) || 0)
34
+ child.left = +(grpW - child.left - cW).toFixed(4)
35
+ }
36
+ }
37
+
38
+ // 2. 垂直翻转:镜像相对垂直位置
39
+ if (flipV) {
40
+ if (typeof child.style?.top === 'string' && child.style.top.endsWith('%')) {
41
+ const tPct = parseFloat(child.style.top) || 0
42
+ const hPct = parseFloat(child.style.height) || 0
43
+ child.style.top = `${+(100 - tPct - hPct).toFixed(4)}%`
44
+ } else if (typeof child.top === 'number' && grpH > 0) {
45
+ const cH = typeof child.height === 'number' ? child.height : (parseFloat(child.style?.height) || 0)
46
+ child.top = +(grpH - child.top - cH).toFixed(4)
47
+ }
48
+ }
49
+
50
+ // 3. 子元素自身翻转状态更新:纯文本和连线永不翻转,形状/图片等反转
51
+ const is2DArrow = /^(?:rightArrow|leftArrow|upArrow|downArrow|leftRightArrow|upDownArrow|quadArrow|leftRightUpArrow|bentArrow|uturnArrow|leftUpArrow|bentUpArrow|curvedRightArrow|curvedLeftArrow|curvedUpArrow|curvedDownArrow|stripedRightArrow|notchedRightArrow|pentagon|chevron|circularArrow|swooshArrow|.*ArrowCallout)$/i.test(String(child.shapType || ''))
52
+ const isConnector = !is2DArrow && (
53
+ child.type === 'line' ||
54
+ Boolean(child.startConnection || child.endConnection) ||
55
+ Boolean(child.borderTailEnd || child.borderHeadEnd) ||
56
+ /^(?:line|straightConnector\d*|bentConnector\d*|curvedConnector\d*)$/i.test(String(child.shapType || '')) ||
57
+ (/连接符/i.test(String(child.name || '')) && !is2DArrow)
58
+ )
59
+ const isPureText = child.type === 'text' || isConnector
60
+
61
+ if (isPureText) {
62
+ child.flipH = false
63
+ child.isFlipH = false
64
+ if (child.style) child.style.flipH = false
65
+ child.flipV = false
66
+ child.isFlipV = false
67
+ if (child.style) child.style.flipV = false
68
+ } else if (child.type === 'group') {
69
+ applyGroupGeometryFlip(child, flipH, flipV)
70
+ } else {
71
+ if (flipH) {
72
+ const curH = Boolean(child.flipH || child.isFlipH || child.style?.flipH)
73
+ child.flipH = !curH
74
+ child.isFlipH = !curH
75
+ if (child.style) child.style.flipH = !curH
76
+ }
77
+ if (flipV) {
78
+ const curV = Boolean(child.flipV || child.isFlipV || child.style?.flipV)
79
+ child.flipV = !curV
80
+ child.isFlipV = !curV
81
+ if (child.style) child.style.flipV = !curV
82
+ }
83
+ }
84
+ }
85
+
86
+ // 4. 子元素全部完成几何镜像后,Group 容器自身无需再 scale 镜像,避免将内部文本整体反转为倒影字
87
+ group.flipH = false
88
+ group.isFlipH = false
89
+ if (group.style) {
90
+ group.style.flipH = false
91
+ group.style.flipV = false
92
+ }
93
+ group.flipV = false
94
+ group.isFlipV = false
95
+ }
96
+
97
+ /**
98
+ * 判断一个元素是否为包含文本的组合元素(或纯文本元素),
99
+ * 渲染层用此判断避免在外层容器施加 scale(-1) 导致内部文字倒影。
100
+ */
101
+ export function isTextContainingElement(el: any): boolean {
102
+ if (!el) return false
103
+ if (el.type === 'text' || el.type === 'line') return true
104
+ if (el.type === 'group' && Array.isArray(el.elements)) {
105
+ return el.elements.some((child: any) => isTextContainingElement(child))
106
+ }
107
+ return false
108
+ }