Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d34b5f532f | |||
| 0f4b03d89f | |||
| d70b80c449 | |||
| 5f02b7c80e |
@@ -12,13 +12,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
||||||
- **Place several images side by side in a row.** A new "Inline (side by
|
|
||||||
side)" alignment mode in the image bubble menu renders consecutive inline
|
|
||||||
images as a row that wraps onto the next line on narrow screens. Unlike the
|
|
||||||
float modes, text does not wrap around inline images. The mode round-trips
|
|
||||||
losslessly through markdown as `data-align`, like the other alignment
|
|
||||||
values.
|
|
||||||
|
|
||||||
- **Editable captions for images.** Images gain an optional caption shown
|
- **Editable captions for images.** Images gain an optional caption shown
|
||||||
below them, edited inline from the image bubble menu and stored as a `caption` attribute. Captions round-trip
|
below them, edited inline from the image bubble menu and stored as a `caption` attribute. Captions round-trip
|
||||||
losslessly through markdown as a `data-caption` attribute on the image, so
|
losslessly through markdown as a `data-caption` attribute on the image, so
|
||||||
|
|||||||
@@ -1322,7 +1322,6 @@
|
|||||||
"Move to space": "Move to space",
|
"Move to space": "Move to space",
|
||||||
"Float left (wrap text)": "Float left (wrap text)",
|
"Float left (wrap text)": "Float left (wrap text)",
|
||||||
"Float right (wrap text)": "Float right (wrap text)",
|
"Float right (wrap text)": "Float right (wrap text)",
|
||||||
"Inline (side by side)": "Inline (side by side)",
|
|
||||||
"Switch to tree": "Switch to tree",
|
"Switch to tree": "Switch to tree",
|
||||||
"Switch to flat list": "Switch to flat list",
|
"Switch to flat list": "Switch to flat list",
|
||||||
"Toggle subpages display mode": "Toggle subpages display mode",
|
"Toggle subpages display mode": "Toggle subpages display mode",
|
||||||
|
|||||||
@@ -1175,7 +1175,6 @@
|
|||||||
"Spoken language hint sent to the transcription model. Auto-detect lets the model decide.": "Подсказка языка речи для модели транскрипции. «Автоопределение» оставляет выбор за моделью.",
|
"Spoken language hint sent to the transcription model. Auto-detect lets the model decide.": "Подсказка языка речи для модели транскрипции. «Автоопределение» оставляет выбор за моделью.",
|
||||||
"Float left (wrap text)": "Обтекание слева",
|
"Float left (wrap text)": "Обтекание слева",
|
||||||
"Float right (wrap text)": "Обтекание справа",
|
"Float right (wrap text)": "Обтекание справа",
|
||||||
"Inline (side by side)": "В ряд",
|
|
||||||
"Switch to tree": "Переключить на дерево",
|
"Switch to tree": "Переключить на дерево",
|
||||||
"Switch to flat list": "Переключить на плоский список",
|
"Switch to flat list": "Переключить на плоский список",
|
||||||
"Toggle subpages display mode": "Переключить режим отображения подстраниц",
|
"Toggle subpages display mode": "Переключить режим отображения подстраниц",
|
||||||
|
|||||||
@@ -15,7 +15,6 @@ import {
|
|||||||
IconLayoutAlignRight,
|
IconLayoutAlignRight,
|
||||||
IconFloatLeft,
|
IconFloatLeft,
|
||||||
IconFloatRight,
|
IconFloatRight,
|
||||||
IconLayoutColumns,
|
|
||||||
IconDownload,
|
IconDownload,
|
||||||
IconRefresh,
|
IconRefresh,
|
||||||
IconTrash,
|
IconTrash,
|
||||||
@@ -47,7 +46,6 @@ export function ImageMenu({ editor }: EditorMenuProps) {
|
|||||||
isAlignRight: ctx.editor.isActive("image", { align: "right" }),
|
isAlignRight: ctx.editor.isActive("image", { align: "right" }),
|
||||||
isFloatLeft: ctx.editor.isActive("image", { align: "floatLeft" }),
|
isFloatLeft: ctx.editor.isActive("image", { align: "floatLeft" }),
|
||||||
isFloatRight: ctx.editor.isActive("image", { align: "floatRight" }),
|
isFloatRight: ctx.editor.isActive("image", { align: "floatRight" }),
|
||||||
isInline: ctx.editor.isActive("image", { align: "inline" }),
|
|
||||||
src: imageAttrs?.src || null,
|
src: imageAttrs?.src || null,
|
||||||
alt: imageAttrs?.alt || "",
|
alt: imageAttrs?.alt || "",
|
||||||
caption: imageAttrs?.caption || "",
|
caption: imageAttrs?.caption || "",
|
||||||
@@ -128,14 +126,6 @@ export function ImageMenu({ editor }: EditorMenuProps) {
|
|||||||
.run();
|
.run();
|
||||||
}, [editor]);
|
}, [editor]);
|
||||||
|
|
||||||
const alignImageInline = useCallback(() => {
|
|
||||||
editor
|
|
||||||
.chain()
|
|
||||||
.focus(undefined, { scrollIntoView: false })
|
|
||||||
.setImageAlign("inline")
|
|
||||||
.run();
|
|
||||||
}, [editor]);
|
|
||||||
|
|
||||||
const handleDownload = useCallback(() => {
|
const handleDownload = useCallback(() => {
|
||||||
if (!editorState?.src) return;
|
if (!editorState?.src) return;
|
||||||
const url = getFileUrl(editorState.src);
|
const url = getFileUrl(editorState.src);
|
||||||
@@ -269,18 +259,6 @@ export function ImageMenu({ editor }: EditorMenuProps) {
|
|||||||
</ActionIcon>
|
</ActionIcon>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
||||||
<Tooltip position="top" label={t("Inline (side by side)")} withinPortal={false}>
|
|
||||||
<ActionIcon
|
|
||||||
onClick={alignImageInline}
|
|
||||||
size="lg"
|
|
||||||
aria-label={t("Inline (side by side)")}
|
|
||||||
variant="subtle"
|
|
||||||
className={clsx({ [classes.active]: editorState?.isInline })}
|
|
||||||
>
|
|
||||||
<IconLayoutColumns size={18} />
|
|
||||||
</ActionIcon>
|
|
||||||
</Tooltip>
|
|
||||||
|
|
||||||
<div className={classes.divider} />
|
<div className={classes.divider} />
|
||||||
|
|
||||||
{altTextButton}
|
{altTextButton}
|
||||||
|
|||||||
@@ -0,0 +1,75 @@
|
|||||||
|
import { describe, it, expect } from "vitest";
|
||||||
|
import {
|
||||||
|
buildLayoutCandidates,
|
||||||
|
getSuggestionItems,
|
||||||
|
} from "./menu-items";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* `buildLayoutCandidates` maps a slash query across physical keyboard layouts
|
||||||
|
* (RU ЙЦУКЕН <-> US QWERTY) so the menu matches Latin item titles/terms even
|
||||||
|
* when typed with the wrong layout active, while keeping the original query so
|
||||||
|
* genuine Cyrillic search terms still match. See bug #283.
|
||||||
|
*/
|
||||||
|
describe("buildLayoutCandidates", () => {
|
||||||
|
it("remaps a RU-layout query to its US-QWERTY equivalent (сщву -> code)", () => {
|
||||||
|
expect(buildLayoutCandidates("сщву")).toContain("code");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("remaps a US-layout query to its RU-ЙЦУКЕН equivalent (cyjcrf -> сноска)", () => {
|
||||||
|
expect(buildLayoutCandidates("cyjcrf")).toContain("сноска");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("always includes the original query", () => {
|
||||||
|
expect(buildLayoutCandidates("сщву")).toContain("сщву");
|
||||||
|
expect(buildLayoutCandidates("cyjcrf")).toContain("cyjcrf");
|
||||||
|
expect(buildLayoutCandidates("сноска")).toContain("сноска");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("leaves a query with no mappable keys as a single-element set", () => {
|
||||||
|
// Digits are on neither layout map, so both remaps are no-ops and de-dup
|
||||||
|
// back to one entry.
|
||||||
|
expect(buildLayoutCandidates("123")).toEqual(["123"]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
/** Helper: flatten grouped suggestion items to a flat list of titles. */
|
||||||
|
const titles = (groups: ReturnType<typeof getSuggestionItems>): string[] =>
|
||||||
|
Object.values(groups).flatMap((items) => items.map((i) => i.title));
|
||||||
|
|
||||||
|
describe("getSuggestionItems layout-aware matching", () => {
|
||||||
|
it("finds Code when 'code' is typed in RU layout (/сщву)", () => {
|
||||||
|
expect(titles(getSuggestionItems({ query: "сщву" }))).toContain("Code");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("still finds Code for the plain /code query", () => {
|
||||||
|
expect(titles(getSuggestionItems({ query: "code" }))).toContain("Code");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("still matches genuine Cyrillic search terms (/сноска -> Footnote)", () => {
|
||||||
|
expect(titles(getSuggestionItems({ query: "сноска" }))).toContain(
|
||||||
|
"Footnote",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("finds Footnote when 'сноска' is typed in EN layout (/cyjcrf)", () => {
|
||||||
|
expect(titles(getSuggestionItems({ query: "cyjcrf" }))).toContain(
|
||||||
|
"Footnote",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not surface Footnote for a short wrong-layout query (/cy)", () => {
|
||||||
|
// "cy" EN->RU remaps to "сн", a substring of the "сноска" searchTerm, but
|
||||||
|
// the gate blocks it because the remapped candidate is < 3 chars.
|
||||||
|
expect(titles(getSuggestionItems({ query: "cy" }))).not.toContain(
|
||||||
|
"Footnote",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not surface Footnote for a single-char wrong-layout query (/b)", () => {
|
||||||
|
// "b" EN->RU remaps to "и", a substring of the "примечание" searchTerm, but
|
||||||
|
// the gate blocks it because the remapped candidate is < 3 chars.
|
||||||
|
expect(titles(getSuggestionItems({ query: "b" }))).not.toContain(
|
||||||
|
"Footnote",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -35,6 +35,7 @@ import { PAGE_EMBED_PICKER_EVENT } from "@/features/editor/components/page-embed
|
|||||||
import {
|
import {
|
||||||
CommandProps,
|
CommandProps,
|
||||||
SlashMenuGroupedItemsType,
|
SlashMenuGroupedItemsType,
|
||||||
|
SlashMenuItemType,
|
||||||
} from "@/features/editor/components/slash-menu/types";
|
} from "@/features/editor/components/slash-menu/types";
|
||||||
import { uploadImageAction } from "@/features/editor/components/image/upload-image-action.tsx";
|
import { uploadImageAction } from "@/features/editor/components/image/upload-image-action.tsx";
|
||||||
import { uploadVideoAction } from "@/features/editor/components/video/upload-video-action.tsx";
|
import { uploadVideoAction } from "@/features/editor/components/video/upload-video-action.tsx";
|
||||||
@@ -835,6 +836,49 @@ export function isHtmlEmbedFeatureEnabled(): boolean {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Russian ЙЦУКЕН -> US QWERTY by physical key position (lowercase; callers
|
||||||
|
// lowercase first). Lets the slash menu match Latin item titles/terms even when
|
||||||
|
// a command is typed with the wrong keyboard layout active (e.g. "/сщву" while
|
||||||
|
// ЙЦУКЕН is on physically types the same keys as "/code").
|
||||||
|
const RU_TO_EN_LAYOUT: Record<string, string> = {
|
||||||
|
й: "q", ц: "w", у: "e", к: "r", е: "t", н: "y", г: "u", ш: "i", щ: "o",
|
||||||
|
з: "p", х: "[", ъ: "]",
|
||||||
|
ф: "a", ы: "s", в: "d", а: "f", п: "g", р: "h", о: "j", л: "k", д: "l",
|
||||||
|
ж: ";", э: "'",
|
||||||
|
я: "z", ч: "x", с: "c", м: "v", и: "b", т: "n", ь: "m", б: ",", ю: ".",
|
||||||
|
ё: "`",
|
||||||
|
};
|
||||||
|
// Inverse map: US QWERTY -> Russian ЙЦУКЕН by physical key position. Handles the
|
||||||
|
// mirror case (e.g. "cyjcrf" typed with EN layout on == "сноска" == Footnote).
|
||||||
|
const EN_TO_RU_LAYOUT: Record<string, string> = Object.fromEntries(
|
||||||
|
Object.entries(RU_TO_EN_LAYOUT).map(([ru, en]) => [en, ru]),
|
||||||
|
);
|
||||||
|
|
||||||
|
function translitByLayout(text: string, map: Record<string, string>): string {
|
||||||
|
let out = "";
|
||||||
|
for (const ch of text) out += map[ch] ?? ch;
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build the list of search strings to try for a given query: the original
|
||||||
|
* query first, followed by its RU->EN and EN->RU physical-layout remappings.
|
||||||
|
* Keeping the original first preserves genuine Cyrillic search terms (e.g.
|
||||||
|
* "сноска"/"примечание" for Footnote) and lets callers treat the original
|
||||||
|
* differently from the remapped candidates. De-duplication only collapses the
|
||||||
|
* list to one element when nothing is remappable (e.g. digits/spaces), so a
|
||||||
|
* typical ASCII query still yields multiple candidates.
|
||||||
|
*/
|
||||||
|
export function buildLayoutCandidates(search: string): string[] {
|
||||||
|
return [
|
||||||
|
...new Set([
|
||||||
|
search,
|
||||||
|
translitByLayout(search, RU_TO_EN_LAYOUT),
|
||||||
|
translitByLayout(search, EN_TO_RU_LAYOUT),
|
||||||
|
]),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
export const getSuggestionItems = ({
|
export const getSuggestionItems = ({
|
||||||
query,
|
query,
|
||||||
excludeItems,
|
excludeItems,
|
||||||
@@ -843,6 +887,18 @@ export const getSuggestionItems = ({
|
|||||||
excludeItems?: Set<string>;
|
excludeItems?: Set<string>;
|
||||||
}): SlashMenuGroupedItemsType => {
|
}): SlashMenuGroupedItemsType => {
|
||||||
const search = query.toLowerCase();
|
const search = query.toLowerCase();
|
||||||
|
const candidates = buildLayoutCandidates(search);
|
||||||
|
// Only the original query is allowed to match via a short substring. Remapped
|
||||||
|
// (wrong-layout) candidates must be at least REMAP_MIN_LEN chars before they
|
||||||
|
// can match, so a 1-2 char ASCII query does not spuriously substring-match
|
||||||
|
// unrelated Cyrillic search terms (e.g. "/cy" -> "сн" hitting "сноска",
|
||||||
|
// "/b" -> "и" hitting "примечание"). buildLayoutCandidates already dedupes
|
||||||
|
// the remaps against the original, so candidates[0] is the original query.
|
||||||
|
const REMAP_MIN_LEN = 3;
|
||||||
|
const [originalCandidate, ...remapped] = candidates;
|
||||||
|
const remappedCandidates = remapped.filter(
|
||||||
|
(candidate) => candidate.length >= REMAP_MIN_LEN,
|
||||||
|
);
|
||||||
const filteredGroups: SlashMenuGroupedItemsType = {};
|
const filteredGroups: SlashMenuGroupedItemsType = {};
|
||||||
const htmlEmbedFeatureEnabled = isHtmlEmbedFeatureEnabled();
|
const htmlEmbedFeatureEnabled = isHtmlEmbedFeatureEnabled();
|
||||||
|
|
||||||
@@ -856,24 +912,42 @@ export const getSuggestionItems = ({
|
|||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const candidateMatchesItem = (
|
||||||
|
candidate: string,
|
||||||
|
item: SlashMenuItemType,
|
||||||
|
description: string,
|
||||||
|
) =>
|
||||||
|
fuzzyMatch(candidate, item.title) ||
|
||||||
|
description.includes(candidate) ||
|
||||||
|
(item.searchTerms != null &&
|
||||||
|
item.searchTerms.some((term: string) => term.includes(candidate)));
|
||||||
|
|
||||||
for (const [group, items] of Object.entries(CommandGroups)) {
|
for (const [group, items] of Object.entries(CommandGroups)) {
|
||||||
const filteredItems = items.filter((item) => {
|
const filteredItems = items.filter((item) => {
|
||||||
if (excludeItems?.has(item.title)) return false;
|
if (excludeItems?.has(item.title)) return false;
|
||||||
// Hide the HTML embed item unless the workspace master toggle is ON.
|
// Hide the HTML embed item unless the workspace master toggle is ON.
|
||||||
if (item.requiresHtmlEmbedFeature && !htmlEmbedFeatureEnabled)
|
if (item.requiresHtmlEmbedFeature && !htmlEmbedFeatureEnabled)
|
||||||
return false;
|
return false;
|
||||||
|
const description = item.description.toLowerCase();
|
||||||
return (
|
return (
|
||||||
fuzzyMatch(search, item.title) ||
|
candidateMatchesItem(originalCandidate, item, description) ||
|
||||||
item.description.toLowerCase().includes(search) ||
|
remappedCandidates.some((candidate) =>
|
||||||
(item.searchTerms &&
|
candidateMatchesItem(candidate, item, description),
|
||||||
item.searchTerms.some((term: string) => term.includes(search)))
|
)
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
if (filteredItems.length) {
|
if (filteredItems.length) {
|
||||||
|
const titleMatchesAnyCandidate = (title: string) => {
|
||||||
|
const lower = title.toLowerCase();
|
||||||
|
return (
|
||||||
|
lower.includes(originalCandidate) ||
|
||||||
|
remappedCandidates.some((candidate) => lower.includes(candidate))
|
||||||
|
);
|
||||||
|
};
|
||||||
filteredGroups[group] = filteredItems.sort((a, b) => {
|
filteredGroups[group] = filteredItems.sort((a, b) => {
|
||||||
const aTitle = a.title.toLowerCase().includes(search) ? 0 : 1;
|
const aTitle = titleMatchesAnyCandidate(a.title) ? 0 : 1;
|
||||||
const bTitle = b.title.toLowerCase().includes(search) ? 0 : 1;
|
const bTitle = titleMatchesAnyCandidate(b.title) ? 0 : 1;
|
||||||
return aTitle - bTitle;
|
return aTitle - bTitle;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -63,38 +63,6 @@ describe("applyAlignment", () => {
|
|||||||
expect(el.dataset.imageAlign).toBe("center");
|
expect(el.dataset.imageAlign).toBe("center");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("inline -> inline-block + top alignment + gap padding, no float", () => {
|
|
||||||
applyAlignment(el, "inline");
|
|
||||||
expect(el.style.display).toBe("inline-block");
|
|
||||||
expect(el.style.verticalAlign).toBe("top");
|
|
||||||
expect(el.style.padding).toBe("0px 10px 10px 0px");
|
|
||||||
expect(el.dataset.imageAlign).toBe("inline");
|
|
||||||
expect(el.style.cssFloat).toBe("");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("clears inline-block when switching inline -> center (reset-then-apply)", () => {
|
|
||||||
applyAlignment(el, "inline");
|
|
||||||
expect(el.style.display).toBe("inline-block");
|
|
||||||
// Switching back to a flex alignment must replace the inline-block
|
|
||||||
// override with the constructor-style flex, not just clear it.
|
|
||||||
applyAlignment(el, "center");
|
|
||||||
expect(el.style.display).toBe("flex");
|
|
||||||
expect(el.style.verticalAlign).toBe("");
|
|
||||||
expect(el.style.padding).toBe("");
|
|
||||||
expect(el.dataset.imageAlign).toBe("center");
|
|
||||||
expect(el.style.justifyContent).toBe("center");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("clears a previous float when switching floatLeft -> inline", () => {
|
|
||||||
applyAlignment(el, "floatLeft");
|
|
||||||
expect(el.style.cssFloat).toBe("left");
|
|
||||||
applyAlignment(el, "inline");
|
|
||||||
expect(el.style.cssFloat).toBe("");
|
|
||||||
expect(el.style.display).toBe("inline-block");
|
|
||||||
expect(el.style.verticalAlign).toBe("top");
|
|
||||||
expect(el.dataset.imageAlign).toBe("inline");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("clears a previous float when switching floatLeft -> left (reset-then-apply)", () => {
|
it("clears a previous float when switching floatLeft -> left (reset-then-apply)", () => {
|
||||||
applyAlignment(el, "floatLeft");
|
applyAlignment(el, "floatLeft");
|
||||||
expect(el.style.cssFloat).toBe("left");
|
expect(el.style.cssFloat).toBe("left");
|
||||||
|
|||||||
@@ -53,13 +53,7 @@ declare module "@tiptap/core" {
|
|||||||
attributes: ImageAttributes & { pos: number | Range },
|
attributes: ImageAttributes & { pos: number | Range },
|
||||||
) => ReturnType;
|
) => ReturnType;
|
||||||
setImageAlign: (
|
setImageAlign: (
|
||||||
align:
|
align: "left" | "center" | "right" | "floatLeft" | "floatRight",
|
||||||
| "left"
|
|
||||||
| "center"
|
|
||||||
| "right"
|
|
||||||
| "floatLeft"
|
|
||||||
| "floatRight"
|
|
||||||
| "inline",
|
|
||||||
) => ReturnType;
|
) => ReturnType;
|
||||||
setImageWidth: (width: number) => ReturnType;
|
setImageWidth: (width: number) => ReturnType;
|
||||||
setImageSize: (width: number, height: number) => ReturnType;
|
setImageSize: (width: number, height: number) => ReturnType;
|
||||||
@@ -421,14 +415,6 @@ export function applyAlignment(container: HTMLElement, align: string) {
|
|||||||
// (a previous float must not leak into a later left/center/right).
|
// (a previous float must not leak into a later left/center/right).
|
||||||
container.style.cssFloat = "";
|
container.style.cssFloat = "";
|
||||||
container.style.padding = "";
|
container.style.padding = "";
|
||||||
// The ResizableNodeView constructor sets an inline `display: flex` on the
|
|
||||||
// container; the inline mode overrides it with `inline-block`, so the reset
|
|
||||||
// restores the constructor's flex here. This keeps the container's layout
|
|
||||||
// independent of any app-level CSS class (which also happens to set flex)
|
|
||||||
// and makes non-inline modes carry exactly the same inline styles as before
|
|
||||||
// the inline mode existed.
|
|
||||||
container.style.display = "flex";
|
|
||||||
container.style.verticalAlign = "";
|
|
||||||
// Mirror the resolved alignment onto the CONTAINER as a data attribute so the
|
// Mirror the resolved alignment onto the CONTAINER as a data attribute so the
|
||||||
// responsive stylesheet can neutralize the float on small screens (an inline
|
// responsive stylesheet can neutralize the float on small screens (an inline
|
||||||
// `float` can only be overridden by `!important`, which keys off this attr).
|
// `float` can only be overridden by `!important`, which keys off this attr).
|
||||||
@@ -444,15 +430,6 @@ export function applyAlignment(container: HTMLElement, align: string) {
|
|||||||
container.style.cssFloat = "right";
|
container.style.cssFloat = "right";
|
||||||
container.style.padding = "0 0 0 10px";
|
container.style.padding = "0 0 0 10px";
|
||||||
container.style.justifyContent = "flex-end";
|
container.style.justifyContent = "flex-end";
|
||||||
} else if (align === "inline") {
|
|
||||||
// Consecutive inline images sit side by side on one line box and wrap to
|
|
||||||
// the next line when the viewport is narrow. The right/bottom padding
|
|
||||||
// provides the gap between images in a row and between wrapped rows;
|
|
||||||
// vertical-align: top keeps rows of different-height images aligned by
|
|
||||||
// their top edge.
|
|
||||||
container.style.display = "inline-block";
|
|
||||||
container.style.verticalAlign = "top";
|
|
||||||
container.style.padding = "0 10px 10px 0";
|
|
||||||
} else if (align === "left") {
|
} else if (align === "left") {
|
||||||
container.style.justifyContent = "flex-start";
|
container.style.justifyContent = "flex-start";
|
||||||
} else if (align === "right") {
|
} else if (align === "right") {
|
||||||
|
|||||||
Reference in New Issue
Block a user