diff --git a/packages/fluent-editor/src/config/editor.utils.ts b/packages/fluent-editor/src/config/editor.utils.ts index de4b30fa..8461a871 100644 --- a/packages/fluent-editor/src/config/editor.utils.ts +++ b/packages/fluent-editor/src/config/editor.utils.ts @@ -105,6 +105,49 @@ export function replaceDeltaImage(delta, imageUrls, imagePlaceholder) { }, new Delta()) } +const WHITE_SPACE_CHAR_RE = /^[\u3000\u0020]$/ +const WHITE_SPACE_OR_NBSP_RE = /^[\u3000\u0020\u00A0]$/ + +/** + * 将连续空格转为不间断空格,以便 innerHTML 输出   并在预览中保留间距。 + * - 行首空白:全部转为 \u00A0 + * - 正文内连续空白:保留第一个普通空格,其余转为 \u00A0 + */ +export function replaceStrWhiteSpace(str: string) { + let textWithWhiteSpace = '' + let beginHasChar = false + let consecutiveSpaces = 0 + for (const char of str) { + if (WHITE_SPACE_CHAR_RE.test(char)) { + consecutiveSpaces += 1 + if (!beginHasChar) { + textWithWhiteSpace += '\u00A0' + } + else if (consecutiveSpaces === 1) { + textWithWhiteSpace += char + } + else { + textWithWhiteSpace += '\u00A0' + } + } + else { + consecutiveSpaces = 0 + textWithWhiteSpace += char + beginHasChar = true + } + } + return textWithWhiteSpace +} + +/** 空格键是否应插入不间断空格(与 replaceStrWhiteSpace 规则一致) */ +export function shouldInsertNbspOnSpace(prefix: string) { + if (!prefix) { + return true + } + const lastChar = prefix.slice(-1) + return WHITE_SPACE_OR_NBSP_RE.test(lastChar) +} + export function splitWithBreak(insertContent: string) { const lines = [] const insertStr = insertContent diff --git a/packages/fluent-editor/src/modules/custom-clipboard.ts b/packages/fluent-editor/src/modules/custom-clipboard.ts index f5bbb7dd..0390e121 100644 --- a/packages/fluent-editor/src/modules/custom-clipboard.ts +++ b/packages/fluent-editor/src/modules/custom-clipboard.ts @@ -12,6 +12,7 @@ import { isNullOrUndefined, omit, replaceDeltaImage, + replaceStrWhiteSpace, splitWithBreak, } from '../config/editor.utils' import { isString } from '../utils/is' @@ -454,22 +455,6 @@ function rebuildDelta(delta, cellLine) { return buildedDelta } -function replaceStrWhiteSpace(str) { - const isWhiteSpace = value => /^(\u3000|\u0020){1}$/.test(value) // 空白字符 - let textWithWhiteSpace = '' - let beginHasChar = false - for (const char of str) { - if (isWhiteSpace(char) && !beginHasChar) { - textWithWhiteSpace += '\u00A0' - } - else { - textWithWhiteSpace += char - beginHasChar = true - } - } - return textWithWhiteSpace -} - function replaceDeltaWhiteSpace(delta, rootBgColor?) { return delta.reduce((newDelta, op) => { // fix: 当粘贴文字颜色和编辑器背景色一致且自身无背景色的情况下移除文字颜色样式,避免误导用户粘贴无效 diff --git a/packages/fluent-editor/src/themes/snow.ts b/packages/fluent-editor/src/themes/snow.ts index 5bcb61b2..c6fcc96d 100644 --- a/packages/fluent-editor/src/themes/snow.ts +++ b/packages/fluent-editor/src/themes/snow.ts @@ -2,7 +2,7 @@ import type { ThemeOptions } from 'quill/core/theme' import type TypeToolbar from 'quill/modules/toolbar' import type TypeIconPicker from 'quill/ui/icon-picker' import { I18N_LOCALE_CHANGE } from 'quill-i18n' -import { inputFile, isNullOrUndefined } from '../config' +import { inputFile, isNullOrUndefined, shouldInsertNbspOnSpace } from '../config' import FluentEditor from '../core/fluent-editor' import { CustomImageSpec } from '../modules/custom-image/specs/custom-image-spec' import { LinkTooltip } from '../modules/link' @@ -21,6 +21,23 @@ OriginSnowTheme.DEFAULTS = { 'keyboard': { bindings: { ...shortKey, + preserveWhiteSpace: { + key: ' ', + collapsed: true, + handler(this: { quill: FluentEditor }, range, context) { + if (!shouldInsertNbspOnSpace(context.prefix)) { + return true + } + this.quill.insertText( + range.index, + '\u00A0', + context.format, + FluentEditor.sources.USER, + ) + this.quill.setSelection(range.index + 1, FluentEditor.sources.SILENT) + return false + }, + }, }, }, 'toolbar': {