jvs-ppt-ui 0.0.16 → 0.0.17

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.
@@ -298,7 +298,7 @@ function onFontSizeEnter(e: KeyboardEvent) {
298
298
  function onFontSizeBlur(e: FocusEvent) {
299
299
  const target = e.target as HTMLInputElement
300
300
  const raw = target.value.trim()
301
- if (raw) {
301
+ if (raw && raw !== fontSizeClean.value) {
302
302
  emit('changeFontSize', raw)
303
303
  }
304
304
  }
@@ -89,9 +89,8 @@ export function useCanvasText(options: TextOptions) {
89
89
 
90
90
  if (textNodes.length === 0) return null
91
91
 
92
- let bestNode: Text | null = null
93
- let bestOffset = 0
94
- let minCost = Infinity
92
+ const charItems: { node: Text; index: number; rect: DOMRect; rowTop: number; rowBottom: number }[] = []
93
+ const rows: { top: number; bottom: number; items: typeof charItems }[] = []
95
94
 
96
95
  for (const node of textNodes) {
97
96
  const len = node.nodeValue?.length || 0
@@ -104,24 +103,51 @@ export function useCanvasText(options: TextOptions) {
104
103
  const charRect = charRange.getBoundingClientRect()
105
104
  if (charRect.width === 0 && charRect.height === 0) continue
106
105
 
107
- const inRow = y >= charRect.top && y <= charRect.bottom
108
- const dy = inRow ? 0 : Math.min(Math.abs(y - charRect.top), Math.abs(y - charRect.bottom))
106
+ const item = { node, index: i, rect: charRect, rowTop: charRect.top, rowBottom: charRect.bottom }
107
+ charItems.push(item)
109
108
 
110
- // 候选点 1:字符左边缘(光标置于 i)
111
- const costLeft = (dy * 200) + Math.abs(x - charRect.left)
112
- if (costLeft < minCost) {
113
- minCost = costLeft
114
- bestNode = node
115
- bestOffset = i
109
+ // 将同一视觉行(垂直方向重叠)的字符归并,共享整行的上下边界,防止同行小字号字符因字高较矮被误判为不在当前行
110
+ let matchedRow = rows.find(r => Math.min(r.bottom, charRect.bottom) > Math.max(r.top, charRect.top) - 2)
111
+ if (matchedRow) {
112
+ matchedRow.top = Math.min(matchedRow.top, charRect.top)
113
+ matchedRow.bottom = Math.max(matchedRow.bottom, charRect.bottom)
114
+ matchedRow.items.push(item)
115
+ } else {
116
+ rows.push({ top: charRect.top, bottom: charRect.bottom, items: [item] })
116
117
  }
118
+ }
119
+ }
117
120
 
118
- // 候选点 2:字符右边缘(光标置于 i + 1)
119
- const costRight = (dy * 200) + Math.abs(x - charRect.right)
120
- if (costRight < minCost) {
121
- minCost = costRight
122
- bestNode = node
123
- bestOffset = i + 1
124
- }
121
+ for (const r of rows) {
122
+ for (const item of r.items) {
123
+ item.rowTop = r.top
124
+ item.rowBottom = r.bottom
125
+ }
126
+ }
127
+
128
+ let bestNode: Text | null = null
129
+ let bestOffset = 0
130
+ let minCost = Infinity
131
+
132
+ for (const item of charItems) {
133
+ const { node, index: i, rect: charRect, rowTop, rowBottom } = item
134
+ const inRow = y >= rowTop && y <= rowBottom
135
+ const dy = inRow ? 0 : Math.min(Math.abs(y - rowTop), Math.abs(y - rowBottom))
136
+
137
+ // 候选点 1:字符左边缘(光标置于 i);在两字符交界处优先保留前一字符右边缘(i + 1),使光标落点归属前一字符
138
+ const costLeft = (dy * 200) + Math.abs(x - charRect.left)
139
+ if (costLeft < minCost - 0.5) {
140
+ minCost = costLeft
141
+ bestNode = node
142
+ bestOffset = i
143
+ }
144
+
145
+ // 候选点 2:字符右边缘(光标置于 i + 1)
146
+ const costRight = (dy * 200) + Math.abs(x - charRect.right)
147
+ if (costRight <= minCost + 0.5) {
148
+ if (costRight < minCost) minCost = costRight
149
+ bestNode = node
150
+ bestOffset = i + 1
125
151
  }
126
152
  }
127
153
 
@@ -16,6 +16,7 @@ import {
16
16
  } from '../../utils/listIndent'
17
17
  import { parseColor, hexToRgba } from './useSidebarShapeFormat'
18
18
  import { getFormatFromElement, getFormatFromSelection } from '../../utils/canvas/formatPainter'
19
+ import { resolveEffectiveElementFontSize } from '../../utils/canvas/elementStyles'
19
20
  import { isEllipseShape } from '../../utils/line'
20
21
  import { defaultStyleRecipe } from '../presetThemes'
21
22
  import {
@@ -66,8 +67,23 @@ export function useSidebarTextFormat(
66
67
  }
67
68
  })
68
69
 
70
+ let savedSidebarRange: Range | null = null
71
+
69
72
  const styleFontSize = computed({
70
- get() { return getStyleVal('fontSize', '16px') },
73
+ get() {
74
+ selectionTrigger.value
75
+ const el = selectedElement.value as any
76
+ if (el) {
77
+ const container = typeof document !== 'undefined' ? document.getElementById(`editable-${el.id}`) : null
78
+ const isEditing = Boolean(container && (container.getAttribute('contenteditable') === 'true' || container === document.activeElement || container.contains(document.activeElement)))
79
+ return resolveEffectiveElementFontSize(el, {
80
+ isEditing,
81
+ savedRange: isEditing ? savedSidebarRange : null,
82
+ fallback: '16px'
83
+ })
84
+ }
85
+ return getStyleVal('fontSize', '16px')
86
+ },
71
87
  set(val: string) { setStyleVal('fontSize', val) }
72
88
  })
73
89
 
@@ -597,7 +613,44 @@ export function useSidebarTextFormat(
597
613
  return
598
614
  }
599
615
  }
616
+
617
+ const el = selectedElement.value as any
618
+ const sel = typeof window !== 'undefined' ? window.getSelection() : null
619
+ const container = el ? document.getElementById(`editable-${el.id}`) : null
620
+ if (container && sel) {
621
+ const activeRange = (sel.rangeCount > 0 && container.contains(sel.getRangeAt(0).commonAncestorContainer))
622
+ ? sel.getRangeAt(0)
623
+ : (savedSidebarRange && container.contains(savedSidebarRange.commonAncestorContainer) ? savedSidebarRange : null)
624
+ if (activeRange && !activeRange.collapsed) {
625
+ sel.removeAllRanges()
626
+ sel.addRange(activeRange)
627
+ document.execCommand('fontSize', false, '7')
628
+ const fontElements = container.querySelectorAll('font[size="7"]')
629
+ if (fontElements.length > 0) {
630
+ fontElements.forEach((font: any) => {
631
+ const span = document.createElement('span')
632
+ span.style.fontSize = formattedSize
633
+ span.innerHTML = font.innerHTML
634
+ span.querySelectorAll('*').forEach((child: any) => {
635
+ if (child.style && child.style.fontSize) {
636
+ child.style.fontSize = ''
637
+ }
638
+ if (child.tagName === 'FONT') {
639
+ child.removeAttribute('size')
640
+ }
641
+ })
642
+ font.parentNode.replaceChild(span, font)
643
+ })
644
+ syncContainerContentToElement(el)
645
+ selectionTrigger.value++
646
+ addHistorySnapshot()
647
+ return
648
+ }
649
+ }
650
+ }
651
+
600
652
  setStyleVal('fontSize', formattedSize)
653
+ selectionTrigger.value++
601
654
  }
602
655
 
603
656
  function syncContainerContentToElement(el: any) {
@@ -739,9 +792,27 @@ export function useSidebarTextFormat(
739
792
  // Selection trigger for tracking lists and caret
740
793
  const selectionTrigger = ref(0)
741
794
  const handleSelectionChange = () => {
795
+ if (typeof window !== 'undefined') {
796
+ const sel = window.getSelection()
797
+ if (sel && sel.rangeCount > 0) {
798
+ const range = sel.getRangeAt(0)
799
+ let node: Node | null = range.commonAncestorContainer
800
+ if (node && node.nodeType !== Node.ELEMENT_NODE) {
801
+ node = node.parentNode
802
+ }
803
+ if (node && (node as HTMLElement).closest && (node as HTMLElement).closest('[id^="editable-"]')) {
804
+ savedSidebarRange = range.cloneRange()
805
+ }
806
+ }
807
+ }
742
808
  selectionTrigger.value++
743
809
  }
744
810
 
811
+ watch(() => selectedElementId.value, () => {
812
+ savedSidebarRange = null
813
+ selectionTrigger.value++
814
+ })
815
+
745
816
  onMounted(() => {
746
817
  document.addEventListener('selectionchange', handleSelectionChange)
747
818
  })