feat(mobile): support image size presets
Add the four desktop-compatible image width presets to the native app and share width configuration across clients.
This commit is contained in:
1 parent
64c3f95991
commit
e90e939815
9 files changed
+254
-35
No files matched your search
@@ -5,10 +5,18 @@ import Placeholder from "@tiptap/extension-placeholder";
|
||||
import { EditorContent, useEditor, useEditorState } from "@tiptap/react";
|
||||
import StarterKit from "@tiptap/starter-kit";
|
||||
import type { TiptapDoc } from "@edgeever/shared";
|
||||
import {
|
||||
DEFAULT_IMAGE_WIDTH_PERCENT,
|
||||
IMAGE_WIDTH_PRESETS,
|
||||
clampImageWidth,
|
||||
parseImageWidth,
|
||||
} from "@edgeever/shared/image-display";
|
||||
import {
|
||||
MOBILE_EDITOR_ACTIVE_FLAGS,
|
||||
MOBILE_EDITOR_TOOLBAR_ACTIONS,
|
||||
getMobileEditorInputAttributes,
|
||||
getMobileEditorImageScaleLabel,
|
||||
getMobileEditorImageWidthPresetLabel,
|
||||
getMobileEditorPlaceholder,
|
||||
getMobileEditorToolbarActionLabel,
|
||||
getMobileEditorToolbarLabel,
|
||||
@@ -436,13 +444,105 @@ const resolveUrl = (source: string, baseUrl: string) => {
|
||||
return `${baseUrl.replace(/\/+$/, "")}${source}`;
|
||||
};
|
||||
|
||||
const applyImageWidth = (
|
||||
element: HTMLElement,
|
||||
attributes: Record<string, unknown>
|
||||
): number => {
|
||||
const width = parseImageWidth(attributes.width) ?? DEFAULT_IMAGE_WIDTH_PERCENT;
|
||||
element.style.width = `${width}%`;
|
||||
element.dataset.width = String(width);
|
||||
return width;
|
||||
};
|
||||
|
||||
const createMobileImageSizeControls = (
|
||||
locale: "zh-CN" | "en-US",
|
||||
updateWidth: (width: number) => void
|
||||
) => {
|
||||
const controls = document.createElement("div");
|
||||
controls.className = "edgeever-image-size-controls";
|
||||
controls.contentEditable = "false";
|
||||
controls.hidden = true;
|
||||
controls.setAttribute("role", "group");
|
||||
controls.setAttribute("aria-label", getMobileEditorImageScaleLabel(locale));
|
||||
|
||||
const buttons = IMAGE_WIDTH_PRESETS.map((preset) => {
|
||||
const button = document.createElement("button");
|
||||
const label = getMobileEditorImageWidthPresetLabel(preset.id, locale);
|
||||
button.type = "button";
|
||||
button.className = "edgeever-image-size-button";
|
||||
button.setAttribute("aria-label", `${label},${preset.width}%`);
|
||||
button.setAttribute("aria-pressed", "false");
|
||||
|
||||
const labelNode = document.createElement("span");
|
||||
labelNode.textContent = label;
|
||||
const percentNode = document.createElement("span");
|
||||
percentNode.className = "edgeever-image-size-percent";
|
||||
percentNode.textContent = `${preset.width}%`;
|
||||
button.append(labelNode, percentNode);
|
||||
|
||||
button.addEventListener("pointerdown", (event) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
});
|
||||
button.addEventListener("click", (event) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
updateWidth(preset.width);
|
||||
});
|
||||
controls.append(button);
|
||||
return { button, width: preset.width };
|
||||
});
|
||||
|
||||
return {
|
||||
dom: controls,
|
||||
setActiveWidth: (width: number) => {
|
||||
for (const item of buttons) {
|
||||
const active = item.width === width;
|
||||
item.button.classList.toggle("is-active", active);
|
||||
item.button.setAttribute("aria-pressed", String(active));
|
||||
}
|
||||
},
|
||||
setVisible: (visible: boolean) => {
|
||||
controls.hidden = !visible;
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
const createProtectedImageExtension = (
|
||||
baseUrl: string,
|
||||
locale: "zh-CN" | "en-US",
|
||||
loadResource: (source: string) => Promise<string | null>
|
||||
) => Image.extend({
|
||||
addAttributes() {
|
||||
return {
|
||||
...this.parent?.(),
|
||||
width: {
|
||||
default: null,
|
||||
parseHTML: (element) =>
|
||||
parseImageWidth(element.getAttribute("data-width") ?? element.getAttribute("width") ?? element.style.width),
|
||||
renderHTML: (attributes) => {
|
||||
const width = parseImageWidth(attributes.width);
|
||||
return width ? { "data-width": String(width), style: `width: ${width}%` } : {};
|
||||
},
|
||||
},
|
||||
};
|
||||
},
|
||||
addNodeView() {
|
||||
return ({ node }) => {
|
||||
return ({ editor, getPos, node }) => {
|
||||
const updateWidth = (width: number) => {
|
||||
const position = getPos();
|
||||
if (typeof position !== "number") {
|
||||
return;
|
||||
}
|
||||
editor
|
||||
.chain()
|
||||
.focus()
|
||||
.setNodeSelection(position)
|
||||
.updateAttributes("image", { width: clampImageWidth(width) })
|
||||
.run();
|
||||
};
|
||||
const sizeControls = createMobileImageSizeControls(locale, updateWidth);
|
||||
|
||||
if (isMobileImageUploadPlaceholderSource(node.attrs.src)) {
|
||||
const placeholder = document.createElement("div");
|
||||
placeholder.className = "edgeever-image-upload-placeholder";
|
||||
@@ -467,10 +567,13 @@ const createProtectedImageExtension = (
|
||||
if (previewSource) {
|
||||
placeholder.append(preview);
|
||||
}
|
||||
placeholder.append(overlay);
|
||||
placeholder.append(overlay, sizeControls.dom);
|
||||
sizeControls.setActiveWidth(applyImageWidth(placeholder, node.attrs));
|
||||
|
||||
let requestId = 0;
|
||||
let renderedSource = String(node.attrs.src ?? "");
|
||||
let completed = false;
|
||||
let selected = false;
|
||||
|
||||
const applyImageAttributes = (attributes: Record<string, unknown>) => {
|
||||
preview.alt = String(attributes.alt ?? "");
|
||||
@@ -497,6 +600,11 @@ const createProtectedImageExtension = (
|
||||
preview.className = "";
|
||||
overlay.remove();
|
||||
placeholder.className = "edgeever-image-upload-result";
|
||||
completed = true;
|
||||
if (selected) {
|
||||
placeholder.classList.add("is-selected");
|
||||
sizeControls.setVisible(true);
|
||||
}
|
||||
placeholder.removeAttribute("role");
|
||||
placeholder.removeAttribute("aria-live");
|
||||
};
|
||||
@@ -534,6 +642,7 @@ const createProtectedImageExtension = (
|
||||
return false;
|
||||
}
|
||||
const source = String(updatedNode.attrs.src ?? "");
|
||||
sizeControls.setActiveWidth(applyImageWidth(placeholder, updatedNode.attrs));
|
||||
if (isMobileImageUploadPlaceholderSource(source)) {
|
||||
return true;
|
||||
}
|
||||
@@ -544,13 +653,29 @@ const createProtectedImageExtension = (
|
||||
loadCompletedImage(updatedNode.attrs);
|
||||
return true;
|
||||
},
|
||||
selectNode: () => {
|
||||
selected = true;
|
||||
if (completed) {
|
||||
placeholder.classList.add("is-selected");
|
||||
sizeControls.setVisible(true);
|
||||
}
|
||||
},
|
||||
deselectNode: () => {
|
||||
selected = false;
|
||||
placeholder.classList.remove("is-selected");
|
||||
sizeControls.setVisible(false);
|
||||
},
|
||||
destroy: () => {
|
||||
requestId += 1;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const wrapper = document.createElement("figure");
|
||||
wrapper.className = "edgeever-image-node";
|
||||
wrapper.contentEditable = "false";
|
||||
const image = document.createElement("img");
|
||||
wrapper.append(image, sizeControls.dom);
|
||||
const imageType = node.type;
|
||||
let requestId = 0;
|
||||
|
||||
@@ -560,6 +685,7 @@ const createProtectedImageExtension = (
|
||||
|
||||
const renderNode = (attributes: Record<string, unknown>) => {
|
||||
clearRequest();
|
||||
sizeControls.setActiveWidth(applyImageWidth(wrapper, attributes));
|
||||
const source = String(attributes.src ?? "");
|
||||
const alt = String(attributes.alt ?? "");
|
||||
const title = String(attributes.title ?? "");
|
||||
@@ -594,7 +720,7 @@ const createProtectedImageExtension = (
|
||||
renderNode(node.attrs);
|
||||
|
||||
return {
|
||||
dom: image,
|
||||
dom: wrapper,
|
||||
update: (updatedNode) => {
|
||||
if (updatedNode.type !== imageType) {
|
||||
return false;
|
||||
@@ -602,6 +728,14 @@ const createProtectedImageExtension = (
|
||||
renderNode(updatedNode.attrs);
|
||||
return true;
|
||||
},
|
||||
selectNode: () => {
|
||||
wrapper.classList.add("is-selected");
|
||||
sizeControls.setVisible(true);
|
||||
},
|
||||
deselectNode: () => {
|
||||
wrapper.classList.remove("is-selected");
|
||||
sizeControls.setVisible(false);
|
||||
},
|
||||
destroy: clearRequest,
|
||||
};
|
||||
};
|
||||
@@ -642,7 +776,12 @@ const insertImageUploadPlaceholder = (
|
||||
editor.chain().command(({ tr, dispatch }) => {
|
||||
const from = Math.min(selection?.from ?? tr.selection.from, tr.doc.content.size);
|
||||
const to = Math.min(Math.max(selection?.to ?? tr.selection.to, from), tr.doc.content.size);
|
||||
tr.replaceRangeWith(from, to, imageType.create({ alt, src: source, title: previewDataUrl }));
|
||||
tr.replaceRangeWith(from, to, imageType.create({
|
||||
alt,
|
||||
src: source,
|
||||
title: previewDataUrl,
|
||||
width: DEFAULT_IMAGE_WIDTH_PERCENT,
|
||||
}));
|
||||
tr.setMeta(TRANSIENT_IMAGE_UPLOAD_META, true);
|
||||
dispatch?.(tr);
|
||||
return true;
|
||||
@@ -704,11 +843,18 @@ const getEditorStyles = (theme: "light" | "dark") => `
|
||||
.edgeever-editor-content code { border-radius: 4px; padding: 2px 4px; background: ${theme === "dark" ? "#1e293b" : "#f1f5f9"}; }
|
||||
.edgeever-editor-content pre code { padding: 0; background: transparent; }
|
||||
.edgeever-editor-content img { display: block; max-width: 100%; height: auto; margin: 14px auto; border-radius: 10px; }
|
||||
.edgeever-image-upload-placeholder { position: relative; min-height: 112px; margin: 14px 0; overflow: hidden; border-radius: 10px; background: ${theme === "dark" ? "#1e293b" : "#f1f5f9"}; }
|
||||
.edgeever-image-upload-placeholder { position: relative; max-width: 100%; min-height: 112px; margin: 14px auto; overflow: hidden; border-radius: 10px; background: ${theme === "dark" ? "#1e293b" : "#f1f5f9"}; }
|
||||
.edgeever-image-upload-preview { display: block; width: 100%; max-height: 360px; margin: 0 !important; object-fit: contain; border-radius: 10px; }
|
||||
.edgeever-image-upload-overlay { position: absolute; inset: 0; display: flex; align-items: center; justify-content: center; gap: 10px; border-radius: 10px; background: rgba(15, 23, 42, 0.38); color: #fff; font-size: 14px; font-weight: 600; text-shadow: 0 1px 2px rgba(15, 23, 42, 0.45); }
|
||||
.edgeever-image-upload-result { margin: 14px 0; }
|
||||
.edgeever-image-upload-result > img { margin: 0 auto; }
|
||||
.edgeever-image-node, .edgeever-image-upload-result { position: relative; display: block; max-width: 100%; margin: 14px auto; line-height: 0; }
|
||||
.edgeever-image-node > img, .edgeever-image-upload-result > img { width: 100%; margin: 0; }
|
||||
.edgeever-image-node.is-selected > img, .edgeever-image-upload-result.is-selected > img { outline: 2px solid #0f766e; outline-offset: 3px; }
|
||||
.edgeever-image-size-controls { position: absolute; left: 50%; bottom: 8px; z-index: 2; display: flex; width: max-content; max-width: calc(100vw - 40px); align-items: center; gap: 3px; transform: translateX(-50%); border: 1px solid ${theme === "dark" ? "#475569" : "#bbf7d0"}; border-radius: 9px; padding: 4px; background: ${theme === "dark" ? "rgba(15, 23, 42, 0.96)" : "rgba(255, 255, 255, 0.96)"}; box-shadow: 0 8px 24px rgba(15, 23, 42, 0.2); line-height: 1.15; }
|
||||
.edgeever-image-size-controls[hidden] { display: none; }
|
||||
.edgeever-image-size-button { display: inline-flex; min-width: 52px; min-height: 44px; appearance: none; flex-direction: column; align-items: center; justify-content: center; gap: 2px; border: 0; border-radius: 7px; padding: 4px 7px; background: transparent; color: ${theme === "dark" ? "#cbd5e1" : "#475569"}; font: inherit; font-size: 12px; font-weight: 700; }
|
||||
.edgeever-image-size-button.is-active { background: ${theme === "dark" ? "#134e4a" : "#ccfbf1"}; color: ${theme === "dark" ? "#99f6e4" : "#0f766e"}; }
|
||||
.edgeever-image-size-percent { color: ${theme === "dark" ? "#94a3b8" : "#94a3b8"}; font-size: 10px; font-weight: 600; }
|
||||
.edgeever-image-size-button.is-active .edgeever-image-size-percent { color: inherit; }
|
||||
.edgeever-image-upload-spinner { width: 18px; height: 18px; border: 2px solid ${theme === "dark" ? "#475569" : "#cbd5e1"}; border-top-color: #0f766e; border-radius: 999px; animation: edgeever-image-upload-spin 0.8s linear infinite; }
|
||||
@keyframes edgeever-image-upload-spin { to { transform: rotate(360deg); } }
|
||||
.edgeever-editor-content hr { margin: 24px 0; border: 0; border-top: 1px solid #cbd5e1; }
|
||||
|
||||
@@ -11,7 +11,7 @@ test("removes transient image upload placeholders before a note is persisted", (
|
||||
content: [
|
||||
{ type: "paragraph", content: [{ type: "text", text: "before" }] },
|
||||
{ type: "image", attrs: { alt: "图片上传中…", src: placeholderSource } },
|
||||
{ type: "image", attrs: { alt: "done", src: "/api/v1/resources/res_1" } },
|
||||
{ type: "image", attrs: { alt: "done", src: "/api/v1/resources/res_1", width: 50 } },
|
||||
],
|
||||
});
|
||||
|
||||
@@ -19,7 +19,7 @@ test("removes transient image upload placeholders before a note is persisted", (
|
||||
type: "doc",
|
||||
content: [
|
||||
{ type: "paragraph", content: [{ type: "text", text: "before" }] },
|
||||
{ type: "image", attrs: { alt: "done", src: "/api/v1/resources/res_1" } },
|
||||
{ type: "image", attrs: { alt: "done", src: "/api/v1/resources/res_1", width: 50 } },
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
@@ -55,7 +55,20 @@ import { RevisionHistoryDialog } from "./dialogs/RevisionHistoryDialog";
|
||||
import { api } from "@/lib/api";
|
||||
import { consumeStandaloneMobileEditorReturn, openStandaloneMobileEditor } from "@/lib/mobile-editor";
|
||||
import { cn, formatDateTime, parseTagsText } from "@/lib/utils";
|
||||
import { docToMarkdown, markdownToDoc, type Notebook, type MemoDetail, type MemoEditSession, type TiptapDoc } from "@edgeever/shared";
|
||||
import {
|
||||
docToMarkdown,
|
||||
markdownToDoc,
|
||||
type Notebook,
|
||||
type MemoDetail,
|
||||
type MemoEditSession,
|
||||
type TiptapDoc,
|
||||
} from "@edgeever/shared";
|
||||
import {
|
||||
DEFAULT_IMAGE_WIDTH_PERCENT,
|
||||
IMAGE_WIDTH_PRESETS,
|
||||
clampImageWidth,
|
||||
parseImageWidth,
|
||||
} from "@edgeever/shared/image-display";
|
||||
import { codeBlockLowlight } from "@/lib/code-block";
|
||||
import { compressImageForUpload } from "@/lib/image-compression";
|
||||
import { localDb, type MemoUpdateSyncPayload } from "@/lib/local-db";
|
||||
@@ -69,15 +82,6 @@ const SUPPORTED_PASTE_IMAGE_TYPES = new Set(["image/png", "image/jpeg", "image/g
|
||||
const MOBILE_EDITOR_QUERY = "(max-width: 639px)";
|
||||
const EDITOR_AUTO_SAVE_DELAY_MS = 1200;
|
||||
const MOBILE_DRAFT_PERSIST_DELAY_MS = 800;
|
||||
const DEFAULT_IMAGE_WIDTH_PERCENT = 72;
|
||||
const MIN_IMAGE_WIDTH_PERCENT = 25;
|
||||
const MAX_IMAGE_WIDTH_PERCENT = 100;
|
||||
const IMAGE_WIDTH_PRESETS = [
|
||||
{ width: 35, labelKey: "editor.imageSizeSmall" },
|
||||
{ width: 50, labelKey: "editor.imageSizeMedium" },
|
||||
{ width: 72, labelKey: "editor.imageSizeLarge" },
|
||||
{ width: 100, labelKey: "editor.imageSizeFull" },
|
||||
] as const;
|
||||
|
||||
type NoteSearchMatch = {
|
||||
from: number;
|
||||
@@ -226,22 +230,6 @@ const getImageFilesFromDataTransfer = (dataTransfer: DataTransfer | null) => {
|
||||
return files.filter((file) => SUPPORTED_PASTE_IMAGE_TYPES.has(file.type));
|
||||
};
|
||||
|
||||
const clampImageWidth = (width: number) =>
|
||||
Math.min(MAX_IMAGE_WIDTH_PERCENT, Math.max(MIN_IMAGE_WIDTH_PERCENT, Math.round(width)));
|
||||
|
||||
const parseImageWidth = (value: unknown) => {
|
||||
if (typeof value === "number" && Number.isFinite(value)) {
|
||||
return clampImageWidth(value);
|
||||
}
|
||||
|
||||
if (typeof value !== "string") {
|
||||
return null;
|
||||
}
|
||||
|
||||
const match = /(\d+(?:\.\d+)?)/.exec(value);
|
||||
return match ? clampImageWidth(Number(match[1])) : null;
|
||||
};
|
||||
|
||||
const ResizableImageNodeView = ({ editor, node, selected, updateAttributes }: NodeViewProps) => {
|
||||
const { t } = useTranslation();
|
||||
const wrapperRef = useRef<HTMLDivElement | null>(null);
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
"type": "module",
|
||||
"exports": {
|
||||
".": "./src/index.ts",
|
||||
"./image-display": "./src/image-display.ts",
|
||||
"./i18n": "./src/i18n/index.ts",
|
||||
"./mobile-editor": "./src/mobile-editor.ts",
|
||||
"./mobile-ui": "./src/mobile-ui.ts"
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import {
|
||||
DEFAULT_IMAGE_WIDTH_PERCENT,
|
||||
IMAGE_WIDTH_PRESETS,
|
||||
clampImageWidth,
|
||||
parseImageWidth,
|
||||
} from "./image-display.ts";
|
||||
|
||||
describe("shared image display widths", () => {
|
||||
test("keeps the four editor presets stable across clients", () => {
|
||||
expect(DEFAULT_IMAGE_WIDTH_PERCENT).toBe(72);
|
||||
expect(IMAGE_WIDTH_PRESETS.map(({ id, width }) => [id, width])).toEqual([
|
||||
["small", 35],
|
||||
["medium", 50],
|
||||
["large", 72],
|
||||
["full", 100],
|
||||
]);
|
||||
});
|
||||
|
||||
test("parses persisted widths and clamps arbitrary resize values", () => {
|
||||
expect(parseImageWidth("width: 50%")).toBe(50);
|
||||
expect(parseImageWidth("120")).toBe(100);
|
||||
expect(parseImageWidth(null)).toBeNull();
|
||||
expect(clampImageWidth(24.6)).toBe(25);
|
||||
expect(clampImageWidth(71.6)).toBe(72);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,28 @@
|
||||
export const DEFAULT_IMAGE_WIDTH_PERCENT = 72;
|
||||
export const MIN_IMAGE_WIDTH_PERCENT = 25;
|
||||
export const MAX_IMAGE_WIDTH_PERCENT = 100;
|
||||
|
||||
export const IMAGE_WIDTH_PRESETS = [
|
||||
{ id: "small", width: 35, labelKey: "editor.imageSizeSmall" },
|
||||
{ id: "medium", width: 50, labelKey: "editor.imageSizeMedium" },
|
||||
{ id: "large", width: 72, labelKey: "editor.imageSizeLarge" },
|
||||
{ id: "full", width: 100, labelKey: "editor.imageSizeFull" },
|
||||
] as const;
|
||||
|
||||
export type ImageWidthPresetId = (typeof IMAGE_WIDTH_PRESETS)[number]["id"];
|
||||
|
||||
export const clampImageWidth = (width: number): number =>
|
||||
Math.min(MAX_IMAGE_WIDTH_PERCENT, Math.max(MIN_IMAGE_WIDTH_PERCENT, Math.round(width)));
|
||||
|
||||
export const parseImageWidth = (value: unknown): number | null => {
|
||||
if (typeof value === "number" && Number.isFinite(value)) {
|
||||
return clampImageWidth(value);
|
||||
}
|
||||
|
||||
if (typeof value !== "string") {
|
||||
return null;
|
||||
}
|
||||
|
||||
const match = /(\d+(?:\.\d+)?)/.exec(value);
|
||||
return match ? clampImageWidth(Number(match[1])) : null;
|
||||
};
|
||||
@@ -1,6 +1,7 @@
|
||||
export * from "./content";
|
||||
export * from "./backup";
|
||||
export * from "./github-feedback";
|
||||
export * from "./image-display";
|
||||
export * from "./memo-safety";
|
||||
export * from "./notebooks";
|
||||
export * from "./revision-diff";
|
||||
|
||||
@@ -3,6 +3,8 @@ import {
|
||||
MOBILE_EDITOR_ACTIVE_FLAGS,
|
||||
MOBILE_EDITOR_TOOLBAR_ACTIONS,
|
||||
getMobileEditorInputAttributes,
|
||||
getMobileEditorImageScaleLabel,
|
||||
getMobileEditorImageWidthPresetLabel,
|
||||
getMobileEditorPlaceholder,
|
||||
getMobileEditorToolbarActionLabel,
|
||||
getMobileEditorToolbarLabel,
|
||||
@@ -27,6 +29,8 @@ describe("mobile editor contract", () => {
|
||||
expect(getMobileEditorPlaceholder("en-US")).toBe("Start writing...");
|
||||
expect(getMobileEditorToolbarLabel("zh-CN")).toBe("编辑器工具栏");
|
||||
expect(getMobileEditorToolbarActionLabel("bulletList", "en-US")).toBe("Bullet list");
|
||||
expect(getMobileEditorImageScaleLabel("zh-CN")).toBe("图片显示尺寸");
|
||||
expect(getMobileEditorImageWidthPresetLabel("medium", "en-US")).toBe("Medium");
|
||||
});
|
||||
|
||||
test("keeps mobile typing assistance enabled", () => {
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import type { ImageWidthPresetId } from "./image-display";
|
||||
|
||||
export type MobileEditorLocale = "zh-CN" | "en-US";
|
||||
|
||||
export type MobileEditorToolbarActionId =
|
||||
@@ -35,6 +37,13 @@ const MOBILE_EDITOR_COPY = {
|
||||
blockquote: "引用",
|
||||
horizontalRule: "分割线",
|
||||
},
|
||||
imageScale: "图片显示尺寸",
|
||||
imageSizes: {
|
||||
small: "较小",
|
||||
medium: "适中",
|
||||
large: "较大",
|
||||
full: "铺满",
|
||||
},
|
||||
},
|
||||
"en-US": {
|
||||
placeholder: "Start writing...",
|
||||
@@ -46,6 +55,13 @@ const MOBILE_EDITOR_COPY = {
|
||||
blockquote: "Quote",
|
||||
horizontalRule: "Horizontal rule",
|
||||
},
|
||||
imageScale: "Image display size",
|
||||
imageSizes: {
|
||||
small: "Small",
|
||||
medium: "Medium",
|
||||
large: "Large",
|
||||
full: "Full",
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
|
||||
@@ -60,6 +76,14 @@ export const getMobileEditorToolbarActionLabel = (
|
||||
locale: MobileEditorLocale
|
||||
): string => MOBILE_EDITOR_COPY[locale].actions[action];
|
||||
|
||||
export const getMobileEditorImageScaleLabel = (locale: MobileEditorLocale): string =>
|
||||
MOBILE_EDITOR_COPY[locale].imageScale;
|
||||
|
||||
export const getMobileEditorImageWidthPresetLabel = (
|
||||
preset: ImageWidthPresetId,
|
||||
locale: MobileEditorLocale
|
||||
): string => MOBILE_EDITOR_COPY[locale].imageSizes[preset];
|
||||
|
||||
export const getMobileEditorInputAttributes = (className: string): Record<string, string> => ({
|
||||
autocapitalize: "sentences",
|
||||
autocomplete: "on",
|
||||
|
||||
Reference in new issue
Block a user