jvs-ppt-ui 0.0.11 → 0.0.12

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.
@@ -80,6 +80,7 @@ const {
80
80
  aiPptDialogVisible,
81
81
  unsplashDialogVisible,
82
82
  isPresenting,
83
+ isReadonly,
83
84
  importSlides,
84
85
  exportSlides,
85
86
  currentEditingFormulaEl,
@@ -110,6 +111,10 @@ const {
110
111
  uploadMediaResourcesInSlides
111
112
  } = slidesState
112
113
 
114
+ watch(() => props.readonly, (val) => {
115
+ isReadonly.value = !!val
116
+ }, { immediate: true })
117
+
113
118
  // 弹层挂载闩锁:首次 visible 后保持挂载(异步组件只加载一次,关闭动画不被 v-if 掐断)
114
119
  function latchMount(source: Ref<boolean>) {
115
120
  const opened = ref(false)
@@ -42,7 +42,7 @@ const {
42
42
  enterMasterView, addMaster, deleteMaster, addLayout, deleteLayout, duplicateLayout,
43
43
  clipboard, slideClipboard, handleUnifiedPaste, sortSlideElements, getEffectiveMasterElementsForSlide,
44
44
  getEffectiveSlideBackground, isCanvasDragging, activeSlideVersion, pauseHistory,
45
- activeSidebarTab
45
+ activeSidebarTab, isReadonly
46
46
  } = useSlides()
47
47
 
48
48
  const displaySlides = shallowRef<any[]>([...slides.value])
@@ -2092,7 +2092,7 @@ const masterGroupedLayouts = computed(() => {
2092
2092
 
2093
2093
  <!-- Regular Sidebar Layouts -->
2094
2094
  <template v-else>
2095
- <div class="sidebar-add-slide-container" style="padding: 12px 16px; display: flex; flex-shrink: 0;">
2095
+ <div v-if="!isReadonly" class="sidebar-add-slide-container" style="padding: 12px 16px; display: flex; flex-shrink: 0;">
2096
2096
  <div class="custom-split-button">
2097
2097
  <!-- Main Add Slide Button -->
2098
2098
  <button @click="addSlide" class="split-btn-left">
@@ -2153,7 +2153,7 @@ const masterGroupedLayouts = computed(() => {
2153
2153
  </button>
2154
2154
  </div> -->
2155
2155
 
2156
- <Draggable v-if="activeSidebarTab === 'outline'" class="slide-list" ref="slideListRef" :modelValue="displaySlides"
2156
+ <Draggable v-if="activeSidebarTab === 'outline'" :disabled="isReadonly" class="slide-list" ref="slideListRef" :modelValue="displaySlides"
2157
2157
  :animation="200" :scroll="true" :scrollSensitivity="50" @end="handleDragEnd" itemKey="id"
2158
2158
  @mouseenter="isMouseOverSlideList = true" @mouseleave="isMouseOverSlideList = false">
2159
2159
  <template #item="{ element: slide, index }">
@@ -2759,6 +2759,17 @@ const masterGroupedLayouts = computed(() => {
2759
2759
  </template>
2760
2760
  </template>
2761
2761
 
2762
+ <template v-else-if="isReadonly">
2763
+ <div class="menu-item" @click="copySlide(rightClickedIndex)">
2764
+ <span class="menu-label">{{ locale === 'zh' ? '复制' : 'Copy' }}</span>
2765
+ <span class="menu-shortcut">Ctrl + C</span>
2766
+ </div>
2767
+ <div class="menu-item" @click="playFromCurrent">
2768
+ <span class="menu-label">{{ locale === 'zh' ? '从当前放映' : 'Play from Current' }}</span>
2769
+ <span class="menu-shortcut">Shift + F5</span>
2770
+ </div>
2771
+ </template>
2772
+
2762
2773
  <template v-else>
2763
2774
  <div class="menu-item" @click="cutSlide(rightClickedIndex)">
2764
2775
  <span class="menu-label">{{ locale === 'zh' ? '剪切' : 'Cut' }}</span>
@@ -22,7 +22,7 @@ import { getFontVariantStyle } from '../../../configs/fonts'
22
22
  import { useSlides } from '../../../composables/useSlides'
23
23
  import { createTextSelectionOverlay, type TextSelectionOverlayHandle } from '../textSelectionOverlay'
24
24
 
25
- const { shapeFormatPainter } = useSlides()
25
+ const { shapeFormatPainter, isReadonly } = useSlides()
26
26
  const isTextPaintActive = computed(() => !!(shapeFormatPainter.value && shapeFormatPainter.value.mode === 'text-range'))
27
27
  const isTextSelectable = computed(() => (props.isEditingText && props.selectedElementId === props.element.id) || isTextPaintActive.value)
28
28
 
@@ -262,7 +262,7 @@ function formatShapeHtml(el: any): string {
262
262
  <div
263
263
  class="element-content shape-element-content"
264
264
  style="width: 100%; height: 100%; position: relative;"
265
- @dblclick.stop.prevent="isImagePlaceholder && isImagePlaceholder(element) ? emit('placeholderUpload', element.id) : (isTextSelectable ? null : emit('startEditing', $event))"
265
+ @dblclick.stop.prevent="isReadonly ? null : (isImagePlaceholder && isImagePlaceholder(element) ? emit('placeholderUpload', element.id) : (isTextSelectable ? null : emit('startEditing', $event)))"
266
266
  >
267
267
  <!-- Circle Shape (with fill) -->
268
268
  <div
@@ -666,7 +666,7 @@ function formatShapeHtml(el: any): string {
666
666
  @keydown="emit('textKeyDown', $event)"
667
667
  @mousemove="emit('mousemove', $event)"
668
668
  @mouseleave="emit('mouseleave', $event)"
669
- @dblclick.stop.prevent="isImagePlaceholder && isImagePlaceholder(element) ? emit('placeholderUpload', element.id) : (isTextSelectable ? null : emit('startEditing', $event))"
669
+ @dblclick.stop.prevent="isReadonly ? null : (isImagePlaceholder && isImagePlaceholder(element) ? emit('placeholderUpload', element.id) : (isTextSelectable ? null : emit('startEditing', $event)))"
670
670
  class="shape-text-overlay"
671
671
  :style="{
672
672
  display: 'block',
@@ -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
  }
@@ -2497,6 +2497,7 @@ watch(formulaDialogVisible, (visible) => {
2497
2497
  }
2498
2498
  })
2499
2499
  const isPresenting = ref(false)
2500
+ const isReadonly = ref(false)
2500
2501
  const showNotification = ref(false)
2501
2502
  const notificationMsg = ref('')
2502
2503
  const shapeFormatPainter = ref<any>(null)
@@ -2958,6 +2959,7 @@ function applyHistoryEntry<T>(entryStrs: string[], currentStrs: string[], list:
2958
2959
  }
2959
2960
 
2960
2961
  function undo() {
2962
+ if (isReadonly.value) return
2961
2963
  if (isMasterEditingView.value) {
2962
2964
  // 先落盘防抖中的快照:编辑后 350ms 内撤销不再丢最后一步编辑
2963
2965
  flushPendingMasterHistorySnapshot()
@@ -3009,6 +3011,7 @@ function undo() {
3009
3011
  }
3010
3012
 
3011
3013
  function redo() {
3014
+ if (isReadonly.value) return
3012
3015
  if (isMasterEditingView.value) {
3013
3016
  flushPendingMasterHistorySnapshot()
3014
3017
  if (masterHistoryIndex.value < masterHistoryStack.value.length - 1) {
@@ -3183,6 +3186,7 @@ function escapeRegExp(string: string) {
3183
3186
  }
3184
3187
 
3185
3188
  function addSlide() {
3189
+ if (isReadonly.value) return
3186
3190
  const newId = Date.now().toString()
3187
3191
  const insertIndex = slides.value.length > 0 ? activeIndex.value + 1 : 0
3188
3192
  const curSlide = slides.value[activeIndex.value]
@@ -3202,6 +3206,7 @@ function addSlide() {
3202
3206
  }
3203
3207
 
3204
3208
  function deleteSlide(index: number) {
3209
+ if (isReadonly.value) return
3205
3210
  const isLastSlide = slides.value.length === 1
3206
3211
  if (isLastSlide) {
3207
3212
  const newSlide = {
@@ -3232,7 +3237,7 @@ function deleteSlide(index: number) {
3232
3237
  }
3233
3238
 
3234
3239
  function pasteSlideData(targetSlideData: any, afterIndex = activeIndex.value, isCut = false) {
3235
- if (!targetSlideData) return
3240
+ if (isReadonly.value || !targetSlideData) return
3236
3241
 
3237
3242
  if (Array.isArray(targetSlideData)) {
3238
3243
  if (targetSlideData.length === 0) return
@@ -3445,7 +3450,7 @@ function scaleElementIfNeeded(el: any, sX: number, sY: number) {
3445
3450
  }
3446
3451
 
3447
3452
  function pasteElementsData(elementsArray: any[], options?: { sourceWidth?: number; sourceHeight?: number }) {
3448
- if (!activeSlide.value || !elementsArray || !Array.isArray(elementsArray) || elementsArray.length === 0) return
3453
+ if (isReadonly.value || !activeSlide.value || !elementsArray || !Array.isArray(elementsArray) || elementsArray.length === 0) return
3449
3454
  const pastedIds: string[] = []
3450
3455
  const oldIdToNewId = new Map<string, string>()
3451
3456
  const oldGroupIdToNewGroupId = new Map<string, string>()
@@ -3889,7 +3894,7 @@ function selectAllSlides() {
3889
3894
  }
3890
3895
 
3891
3896
  function deleteSelectedSlides() {
3892
- if (!slides.value || slides.value.length === 0) return
3897
+ if (isReadonly.value || !slides.value || slides.value.length === 0) return
3893
3898
  const sortedIndexes = [...selectedSlideIndexes.value].filter(i => i >= 0 && i < slides.value.length).sort((a, b) => b - a)
3894
3899
  if (sortedIndexes.length === 0) {
3895
3900
  deleteSlide(activeIndex.value)
@@ -3942,12 +3947,14 @@ function copySelectedSlides() {
3942
3947
  }
3943
3948
 
3944
3949
  function cutSelectedSlides() {
3950
+ if (isReadonly.value) return
3945
3951
  copySelectedSlides()
3946
3952
  deleteSelectedSlides()
3947
3953
  notify('已剪切选中的幻灯片')
3948
3954
  }
3949
3955
 
3950
3956
  function duplicateSelectedSlides() {
3957
+ if (isReadonly.value) return
3951
3958
  const indexes = [...selectedSlideIndexes.value].filter(i => i >= 0 && i < slides.value.length).sort((a, b) => a - b)
3952
3959
  if (indexes.length === 0) indexes.push(activeIndex.value)
3953
3960
  const lastIndex = indexes[indexes.length - 1]
@@ -3976,6 +3983,7 @@ function duplicateSelectedSlides() {
3976
3983
  }
3977
3984
 
3978
3985
  function applyBackground(bg: string) {
3986
+ if (isReadonly.value) return
3979
3987
  if (activeSlide.value) {
3980
3988
  activeSlide.value.background = bg
3981
3989
  activeSlide.value.bgType = 'gradient'
@@ -4009,7 +4017,7 @@ function addElement(
4009
4017
  initialDimensions?: { width?: number; height?: number; left?: number; top?: number },
4010
4018
  extraOptions?: { textType?: SlideTextType; name?: string; [key: string]: any }
4011
4019
  ) {
4012
- if (!activeSlide.value) return
4020
+ if (isReadonly.value || !activeSlide.value) return
4013
4021
 
4014
4022
  const recipe = getActiveSlideThemeRecipe(activeSlide.value)
4015
4023
 
@@ -4824,7 +4832,7 @@ function updateGroupBounds(groupEl: SlideElement) {
4824
4832
 
4825
4833
  // Delete Element from current slide
4826
4834
  function deleteElement(id: string, silent = false) {
4827
- if (!activeSlide.value) return
4835
+ if (isReadonly.value || !activeSlide.value) return
4828
4836
  const hasTopLevel = activeSlide.value.elements.some(item => item.id === id)
4829
4837
  if (hasTopLevel) {
4830
4838
  activeSlide.value.elements = activeSlide.value.elements.filter(item => item.id !== id)
@@ -9255,6 +9263,7 @@ export function useSlides() {
9255
9263
  selectedElementIds,
9256
9264
  croppingElementId,
9257
9265
  isPresenting,
9266
+ isReadonly,
9258
9267
  showNotification,
9259
9268
  notificationMsg,
9260
9269
  notify,