jvs-ppt-ui 0.0.16 → 0.0.18

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.
@@ -964,3 +964,259 @@ export function updateTextAlignInHtml(html: string, align: string): string {
964
964
  return `style="${newStyle}"`
965
965
  })
966
966
  }
967
+
968
+
969
+ /**
970
+ * Resolve the effective font size (in px) for a text/shape element:
971
+ * - When the cursor/selection is active inside the element's editable container:
972
+ * - For a collapsed cursor (caret), returns the font size of the character immediately BEFORE the cursor
973
+ * on the current line/block (falling back to the character after the cursor if at the very start of the line).
974
+ * - For a non-collapsed range selection, returns the font size of the first selected character.
975
+ * - When there is no cursor in the element, returns the font size of the first line (first non-empty character).
976
+ */
977
+ export function resolveEffectiveElementFontSize(
978
+ el: any,
979
+ options?: {
980
+ isEditing?: boolean
981
+ savedRange?: Range | null
982
+ fallback?: string
983
+ }
984
+ ): string {
985
+ const fallback = options?.fallback || '18px'
986
+ if (!el) return fallback
987
+
988
+ const parseSizeToPx = (raw: any): string | null => {
989
+ if (raw === undefined || raw === null || raw === '' || raw === 'auto') return null
990
+ const match = String(raw).trim().match(/(\d+(?:\.\d+)?)\s*(px|pt)?/i)
991
+ if (match && match[1]) {
992
+ let size = parseFloat(match[1])
993
+ const unit = (match[2] || 'px').toLowerCase()
994
+ if (unit === 'pt') {
995
+ size = size * 1.33333
996
+ }
997
+ if (!isNaN(size) && size > 0) {
998
+ return `${Math.round(size)}px`
999
+ }
1000
+ }
1001
+ return null
1002
+ }
1003
+
1004
+ const resolveFontSizeFromDomNode = (targetNode: Node | null, container: HTMLElement): string | null => {
1005
+ if (!targetNode) return null
1006
+ let cur: HTMLElement | null = targetNode.nodeType === Node.ELEMENT_NODE
1007
+ ? (targetNode as HTMLElement)
1008
+ : targetNode.parentElement
1009
+
1010
+ // 1. Walk up from targetNode to container (exclusive) checking inline font-size
1011
+ while (cur && cur !== container && container.contains(cur)) {
1012
+ const styleAttr = cur.getAttribute('style') || ''
1013
+ const match = styleAttr.match(/font-size\s*:\s*(\d+(?:\.\d+)?)\s*(px|pt)?/i)
1014
+ if (match && match[1]) {
1015
+ const parsed = parseSizeToPx(`${match[1]}${match[2] || 'px'}`)
1016
+ if (parsed) return parsed
1017
+ }
1018
+ if (cur.style && cur.style.fontSize) {
1019
+ const parsed = parseSizeToPx(cur.style.fontSize)
1020
+ if (parsed) return parsed
1021
+ }
1022
+ cur = cur.parentElement
1023
+ }
1024
+
1025
+ // 2. Fallback to element's style.fontSize or container's inline font-size
1026
+ if (el.style && el.style.fontSize && el.style.fontSize !== 'auto') {
1027
+ const parsed = parseSizeToPx(el.style.fontSize)
1028
+ if (parsed) return parsed
1029
+ }
1030
+ if (el.fontSize && el.fontSize !== 'auto') {
1031
+ const parsed = parseSizeToPx(el.fontSize)
1032
+ if (parsed) return parsed
1033
+ }
1034
+ const containerStyleAttr = container.getAttribute('style') || ''
1035
+ const containerMatch = containerStyleAttr.match(/font-size\s*:\s*(\d+(?:\.\d+)?)\s*(px|pt)?/i)
1036
+ if (containerMatch && containerMatch[1]) {
1037
+ const parsed = parseSizeToPx(`${containerMatch[1]}${containerMatch[2] || 'px'}`)
1038
+ if (parsed) return parsed
1039
+ }
1040
+
1041
+ // 3. Fallback to computed style on the element
1042
+ const elem = targetNode.nodeType === Node.ELEMENT_NODE
1043
+ ? (targetNode as HTMLElement)
1044
+ : targetNode.parentElement
1045
+ if (elem && typeof window !== 'undefined') {
1046
+ const computedFs = window.getComputedStyle(elem).fontSize
1047
+ const parsed = parseSizeToPx(computedFs)
1048
+ if (parsed) return parsed
1049
+ }
1050
+
1051
+ return null
1052
+ }
1053
+
1054
+ const container = typeof document !== 'undefined' && el.id
1055
+ ? (document.getElementById(`editable-${el.id}`) || document.getElementById(`editable-element-${el.id}`) || document.getElementById(`screen-editable-${el.id}`))
1056
+ : null
1057
+
1058
+ const isEditing = options?.isEditing !== undefined
1059
+ ? options.isEditing
1060
+ : Boolean(container && (container.getAttribute('contenteditable') === 'true' || container === document.activeElement || container.contains(document.activeElement)))
1061
+
1062
+ // 1. Cursor / selection is active inside the element
1063
+ if (container && isEditing && typeof window !== 'undefined') {
1064
+ const selectedTd = container.querySelector('td.selected-cell') as HTMLElement | null
1065
+ if (selectedTd) {
1066
+ const tdWalker = document.createTreeWalker(selectedTd, NodeFilter.SHOW_TEXT, null)
1067
+ let firstTdText: Text | null = null
1068
+ let tdNode: Node | null = tdWalker.nextNode()
1069
+ while (tdNode) {
1070
+ if ((tdNode.nodeValue || '').replace(/[\u200B\r\n\s]/g, '').length > 0) {
1071
+ firstTdText = tdNode as Text
1072
+ break
1073
+ }
1074
+ tdNode = tdWalker.nextNode()
1075
+ }
1076
+ const tdFs = resolveFontSizeFromDomNode(firstTdText || selectedTd, container)
1077
+ if (tdFs) return tdFs
1078
+ }
1079
+
1080
+ const sel = window.getSelection()
1081
+ let activeRange: Range | null = null
1082
+ if (sel && sel.rangeCount > 0) {
1083
+ const r = sel.getRangeAt(0)
1084
+ if (container.contains(r.commonAncestorContainer)) {
1085
+ activeRange = r
1086
+ }
1087
+ }
1088
+ if (!activeRange && options?.savedRange && container.contains(options.savedRange.commonAncestorContainer)) {
1089
+ activeRange = options.savedRange
1090
+ }
1091
+
1092
+ if (activeRange) {
1093
+ if (!activeRange.collapsed) {
1094
+ const startRange = activeRange.cloneRange()
1095
+ startRange.collapse(true)
1096
+ const endRange = activeRange.cloneRange()
1097
+ endRange.collapse(false)
1098
+
1099
+ const walker = document.createTreeWalker(container, NodeFilter.SHOW_TEXT, null)
1100
+ let firstSelectedText: Text | null = null
1101
+ let node: Node | null = walker.nextNode()
1102
+ while (node) {
1103
+ const rawVal = node.nodeValue || ''
1104
+ if (rawVal.replace(/[\u200B\r\n]/g, '').length > 0) {
1105
+ try {
1106
+ if (startRange.comparePoint(node, rawVal.length) > 0 && endRange.comparePoint(node, 0) < 0) {
1107
+ firstSelectedText = node as Text
1108
+ break
1109
+ }
1110
+ } catch (_) {}
1111
+ }
1112
+ node = walker.nextNode()
1113
+ }
1114
+ const targetNode = firstSelectedText || activeRange.startContainer
1115
+ const fs = resolveFontSizeFromDomNode(targetNode, container)
1116
+ if (fs) return fs
1117
+ } else {
1118
+ let blockRoot: HTMLElement = container
1119
+ let temp: Node | null = activeRange.startContainer
1120
+ while (temp && temp !== container) {
1121
+ if (temp.nodeType === Node.ELEMENT_NODE && /^(P|DIV|LI|H[1-6]|BLOCKQUOTE|TD|TH)$/i.test((temp as HTMLElement).tagName)) {
1122
+ blockRoot = temp as HTMLElement
1123
+ break
1124
+ }
1125
+ temp = temp.parentNode
1126
+ }
1127
+
1128
+ let prevTextNode: Text | null = null
1129
+ let nextTextNode: Text | null = null
1130
+ const walker = document.createTreeWalker(blockRoot, NodeFilter.SHOW_TEXT | NodeFilter.SHOW_ELEMENT, null)
1131
+ let node: Node | null = walker.nextNode()
1132
+ while (node) {
1133
+ if (node.nodeType === Node.ELEMENT_NODE && (node as HTMLElement).tagName === 'BR') {
1134
+ try {
1135
+ if (activeRange.comparePoint(node, 0) < 0) {
1136
+ prevTextNode = null
1137
+ } else if (nextTextNode) {
1138
+ break
1139
+ }
1140
+ } catch (_) {}
1141
+ } else if (node.nodeType === Node.TEXT_NODE) {
1142
+ const rawVal = node.nodeValue || ''
1143
+ const firstCharIdx = rawVal.search(/[^\u200B\r\n]/)
1144
+ if (firstCharIdx >= 0) {
1145
+ try {
1146
+ // 若该文本节点首个可见字符结束位置 (firstCharIdx + 1) 在光标位置之前或恰好重合,说明光标前存在该节点的可见字符
1147
+ const cmp = activeRange.comparePoint(node, firstCharIdx + 1)
1148
+ if (cmp <= 0) {
1149
+ prevTextNode = node as Text
1150
+ } else if (!nextTextNode) {
1151
+ nextTextNode = node as Text
1152
+ }
1153
+ } catch (_) {}
1154
+ }
1155
+ }
1156
+ node = walker.nextNode()
1157
+ }
1158
+
1159
+ const targetNode = prevTextNode || nextTextNode || activeRange.startContainer
1160
+ const fs = resolveFontSizeFromDomNode(targetNode, container)
1161
+ if (fs) return fs
1162
+ }
1163
+ }
1164
+ }
1165
+
1166
+ // 2. No active cursor: use the font size of the first line (first non-empty text character)
1167
+ if (container) {
1168
+ const walker = document.createTreeWalker(container, NodeFilter.SHOW_TEXT, null)
1169
+ let firstTextNode: Text | null = null
1170
+ let node: Node | null = walker.nextNode()
1171
+ while (node) {
1172
+ if ((node.nodeValue || '').replace(/[\u200B\r\n\s]/g, '').length > 0) {
1173
+ firstTextNode = node as Text
1174
+ break
1175
+ }
1176
+ node = walker.nextNode()
1177
+ }
1178
+ if (firstTextNode) {
1179
+ const fs = resolveFontSizeFromDomNode(firstTextNode, container)
1180
+ if (fs) return fs
1181
+ }
1182
+ }
1183
+
1184
+ // 3. Fallback when DOM container is not mounted or has no text nodes
1185
+ const htmlContent = el.content || (typeof el.text === 'string' ? el.text : el.text?.content) || ''
1186
+ if (htmlContent) {
1187
+ if (typeof document !== 'undefined' && htmlContent.includes('<')) {
1188
+ const tempDiv = document.createElement('div')
1189
+ tempDiv.innerHTML = htmlContent
1190
+ const walker = document.createTreeWalker(tempDiv, NodeFilter.SHOW_TEXT, null)
1191
+ let firstTextNode: Text | null = null
1192
+ let node: Node | null = walker.nextNode()
1193
+ while (node) {
1194
+ if ((node.nodeValue || '').replace(/[\u200B\r\n\s]/g, '').length > 0) {
1195
+ firstTextNode = node as Text
1196
+ break
1197
+ }
1198
+ node = walker.nextNode()
1199
+ }
1200
+ if (firstTextNode) {
1201
+ const fs = resolveFontSizeFromDomNode(firstTextNode, tempDiv)
1202
+ if (fs) return fs
1203
+ }
1204
+ }
1205
+ const match = htmlContent.match(/font-size\s*:\s*(\d+(?:\.\d+)?)\s*(px|pt)?/i)
1206
+ if (match && match[1]) {
1207
+ const parsed = parseSizeToPx(`${match[1]}${match[2] || 'px'}`)
1208
+ if (parsed) return parsed
1209
+ }
1210
+ }
1211
+
1212
+ if (el.style && el.style.fontSize && el.style.fontSize !== 'auto') {
1213
+ const parsed = parseSizeToPx(el.style.fontSize)
1214
+ if (parsed) return parsed
1215
+ }
1216
+ if (el.fontSize && el.fontSize !== 'auto') {
1217
+ const parsed = parseSizeToPx(el.fontSize)
1218
+ if (parsed) return parsed
1219
+ }
1220
+
1221
+ return fallback
1222
+ }