mirror of
https://github.com/discourse/discourse.git
synced 2024-11-22 05:01:05 +08:00
FEATURE: image grid in posts (experimental) (#21513)
Adds a new `[grid]` tag that can arrange images (or other media) into a grid in posts. The grid defaults to a 3-column with a few exceptions: - if there are only 2 or 4 items, it defaults to a 2-column grid (because it generally looks better) - on mobile, it defaults to a 2-column grid - if there is only one item, the grid has no effect
This commit is contained in:
parent
e43ac00bf4
commit
987ec602ec
|
@ -742,12 +742,44 @@ export default Component.extend(
|
|||
);
|
||||
},
|
||||
|
||||
@bind
|
||||
_handleImageGridButtonClick(event) {
|
||||
if (!event.target.classList.contains("wrap-image-grid-button")) {
|
||||
return;
|
||||
}
|
||||
|
||||
const index = parseInt(
|
||||
event.target.closest(".button-wrapper").dataset.imageIndex,
|
||||
10
|
||||
);
|
||||
const reply = this.get("composer.reply");
|
||||
const matches = reply.match(IMAGE_MARKDOWN_REGEX);
|
||||
const closingIndex =
|
||||
index + parseInt(event.target.dataset.imageCount, 10) - 1;
|
||||
|
||||
const textArea = this.element.querySelector(".d-editor-input");
|
||||
textArea.selectionStart = reply.indexOf(matches[index]);
|
||||
textArea.selectionEnd =
|
||||
reply.indexOf(matches[closingIndex]) + matches[closingIndex].length;
|
||||
|
||||
this.appEvents.trigger(
|
||||
`${this.composerEventPrefix}:apply-surround`,
|
||||
"[grid]",
|
||||
"[/grid]",
|
||||
"grid_surround",
|
||||
{ useBlockMode: true }
|
||||
);
|
||||
},
|
||||
|
||||
_registerImageAltTextButtonClick(preview) {
|
||||
preview.addEventListener("click", this._handleAltTextEditButtonClick);
|
||||
preview.addEventListener("click", this._handleAltTextOkButtonClick);
|
||||
preview.addEventListener("click", this._handleAltTextCancelButtonClick);
|
||||
preview.addEventListener("click", this._handleImageDeleteButtonClick);
|
||||
preview.addEventListener("keypress", this._handleAltTextInputKeypress);
|
||||
if (this.siteSettings.experimental_post_image_grid) {
|
||||
preview.addEventListener("click", this._handleImageGridButtonClick);
|
||||
}
|
||||
},
|
||||
|
||||
@on("willDestroyElement")
|
||||
|
@ -773,6 +805,10 @@ export default Component.extend(
|
|||
preview?.removeEventListener("click", this._handleImageScaleButtonClick);
|
||||
preview?.removeEventListener("click", this._handleAltTextEditButtonClick);
|
||||
preview?.removeEventListener("click", this._handleAltTextOkButtonClick);
|
||||
preview?.removeEventListener("click", this._handleImageDeleteButtonClick);
|
||||
if (this.siteSettings.experimental_post_image_grid) {
|
||||
preview?.removeEventListener("click", this._handleImageGridButtonClick);
|
||||
}
|
||||
preview?.removeEventListener(
|
||||
"click",
|
||||
this._handleAltTextCancelButtonClick
|
||||
|
|
|
@ -309,6 +309,7 @@ export default Component.extend(TextareaTextManipulation, {
|
|||
this.appEvents.on("composer:insert-block", this, "insertBlock");
|
||||
this.appEvents.on("composer:insert-text", this, "insertText");
|
||||
this.appEvents.on("composer:replace-text", this, "replaceText");
|
||||
this.appEvents.on("composer:apply-surround", this, "_applySurround");
|
||||
this.appEvents.on(
|
||||
"composer:indent-selected-text",
|
||||
this,
|
||||
|
@ -349,6 +350,7 @@ export default Component.extend(TextareaTextManipulation, {
|
|||
this.appEvents.off("composer:insert-block", this, "insertBlock");
|
||||
this.appEvents.off("composer:insert-text", this, "insertText");
|
||||
this.appEvents.off("composer:replace-text", this, "replaceText");
|
||||
this.appEvents.off("composer:apply-surround", this, "_applySurround");
|
||||
this.appEvents.off(
|
||||
"composer:indent-selected-text",
|
||||
this,
|
||||
|
@ -646,6 +648,11 @@ export default Component.extend(TextareaTextManipulation, {
|
|||
}
|
||||
},
|
||||
|
||||
_applySurround(head, tail, exampleKey, opts) {
|
||||
const selected = this.getSelected();
|
||||
this.applySurround(selected, head, tail, exampleKey, opts);
|
||||
},
|
||||
|
||||
_toggleDirection() {
|
||||
let currentDir = this._$textarea.attr("dir")
|
||||
? this._$textarea.attr("dir")
|
||||
|
|
|
@ -3,6 +3,7 @@ import discourseLater from "discourse-common/lib/later";
|
|||
import I18n from "I18n";
|
||||
import highlightSyntax from "discourse/lib/highlight-syntax";
|
||||
import lightbox from "discourse/lib/lightbox";
|
||||
import Columns from "discourse/lib/columns";
|
||||
import { iconHTML, iconNode } from "discourse-common/lib/icon-library";
|
||||
import { setTextDirections } from "discourse/lib/text-direction";
|
||||
import { nativeLazyLoading } from "discourse/lib/lazy-load-images";
|
||||
|
@ -33,6 +34,25 @@ export default {
|
|||
{ id: "discourse-lightbox" }
|
||||
);
|
||||
|
||||
if (siteSettings.experimental_post_image_grid) {
|
||||
api.decorateCookedElement(
|
||||
(elem) => {
|
||||
const grids = elem.querySelectorAll(".d-image-grid");
|
||||
|
||||
if (!grids.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
grids.forEach((grid) => {
|
||||
return new Columns(grid, {
|
||||
columns: site.mobileView ? 2 : 3,
|
||||
});
|
||||
});
|
||||
},
|
||||
{ id: "discourse-image-grid" }
|
||||
);
|
||||
}
|
||||
|
||||
if (siteSettings.support_mixed_text_direction) {
|
||||
api.decorateCookedElement(setTextDirections, {
|
||||
id: "discourse-text-direction",
|
||||
|
|
110
app/assets/javascripts/discourse/app/lib/columns.js
Normal file
110
app/assets/javascripts/discourse/app/lib/columns.js
Normal file
|
@ -0,0 +1,110 @@
|
|||
/**
|
||||
* Turns an element containing multiple children into a grid of columns.
|
||||
* Can be used to arrange images or media in a grid.
|
||||
*
|
||||
* Inspired/adapted from https://github.com/mladenilic/columns.js
|
||||
*
|
||||
* TODO: Add unit tests
|
||||
*/
|
||||
export default class Columns {
|
||||
constructor(container, options = {}) {
|
||||
this.container = container;
|
||||
|
||||
this.options = {
|
||||
columns: 3,
|
||||
columnClass: "d-image-grid-column",
|
||||
minCount: 2,
|
||||
...options,
|
||||
};
|
||||
|
||||
this.excluded = ["BR", "P"];
|
||||
|
||||
this.items = this._prepareItems();
|
||||
|
||||
if (this.items.length >= this.options.minCount) {
|
||||
this.render();
|
||||
} else {
|
||||
container.dataset.disabled = true;
|
||||
}
|
||||
}
|
||||
|
||||
count() {
|
||||
// a 2x2 grid looks better in most cases for 2 or 4 items
|
||||
if (this.items.length === 4 || this.items.length === 2) {
|
||||
return 2;
|
||||
}
|
||||
return this.options.columns;
|
||||
}
|
||||
|
||||
render() {
|
||||
if (this.container.dataset.columns) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.container.dataset.columns = this.count();
|
||||
|
||||
const columns = this._distributeEvenly();
|
||||
|
||||
while (this.container.firstChild) {
|
||||
this.container.removeChild(this.container.firstChild);
|
||||
}
|
||||
this.container.append(...columns);
|
||||
return this;
|
||||
}
|
||||
|
||||
_prepareColumns(count) {
|
||||
const columns = [];
|
||||
[...Array(count)].forEach(() => {
|
||||
const column = document.createElement("div");
|
||||
column.classList.add(this.options.columnClass);
|
||||
columns.push(column);
|
||||
});
|
||||
|
||||
return columns;
|
||||
}
|
||||
|
||||
_prepareItems() {
|
||||
let targets = [];
|
||||
|
||||
Array.from(this.container.children).forEach((child) => {
|
||||
if (child.nodeName === "P" && child.children.length > 0) {
|
||||
// sometimes children are wrapped in a paragraph
|
||||
targets.push(...child.children);
|
||||
} else {
|
||||
targets.push(child);
|
||||
}
|
||||
});
|
||||
|
||||
return targets.filter((item) => {
|
||||
return !this.excluded.includes(item.nodeName);
|
||||
});
|
||||
}
|
||||
|
||||
_distributeEvenly() {
|
||||
const count = this.count();
|
||||
const columns = this._prepareColumns(count);
|
||||
|
||||
const columnHeights = [];
|
||||
for (let n = 0; n < count; n++) {
|
||||
columnHeights[n] = 0;
|
||||
}
|
||||
this.items.forEach((item) => {
|
||||
let shortest = 0;
|
||||
|
||||
for (let j = 1; j < count; ++j) {
|
||||
if (columnHeights[j] < columnHeights[shortest]) {
|
||||
shortest = j;
|
||||
}
|
||||
}
|
||||
|
||||
// use aspect ratio to compare heights and append to shortest column
|
||||
// if element is not an image, assue ratio is 1:1
|
||||
const img = item.querySelector("img") || item;
|
||||
const aR = img.nodeName === "IMG" ? img.height / img.width : 1;
|
||||
columnHeights[shortest] += aR;
|
||||
columns[shortest].append(item);
|
||||
});
|
||||
|
||||
return columns;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,148 @@
|
|||
import { click, fillIn, visit } from "@ember/test-helpers";
|
||||
import { acceptance, query } from "discourse/tests/helpers/qunit-helpers";
|
||||
import { test } from "qunit";
|
||||
|
||||
acceptance("Composer - Image Grid", function (needs) {
|
||||
needs.user();
|
||||
needs.settings({
|
||||
experimental_post_image_grid: true,
|
||||
allow_uncategorized_topics: true,
|
||||
});
|
||||
|
||||
needs.pretender((server, helper) => {
|
||||
server.post("/uploads/lookup-urls", () => {
|
||||
return helper.response([]);
|
||||
});
|
||||
});
|
||||
|
||||
test("Image Grid", async function (assert) {
|
||||
await visit("/");
|
||||
|
||||
const uploads = [
|
||||
"![image_example_0|666x500](upload://q4iRxcuSAzfnbUaCsbjMXcGrpaK.jpeg)",
|
||||
"![image_example_1|481x480](upload://p1ijebM2iyQcUswBffKwMny3gxu.jpeg)",
|
||||
"![image_example_3|481x480](upload://p1ijebM2iyQcUswBffKwMny3gxu.jpeg)",
|
||||
];
|
||||
|
||||
await click("#create-topic");
|
||||
await fillIn(".d-editor-input", uploads.join("\n"));
|
||||
|
||||
await click(
|
||||
".button-wrapper[data-image-index='0'] .wrap-image-grid-button"
|
||||
);
|
||||
|
||||
assert.strictEqual(
|
||||
query(".d-editor-input").value,
|
||||
`[grid]\n${uploads.join("\n")}\n[/grid]`,
|
||||
"Image grid toggles on"
|
||||
);
|
||||
|
||||
await click(
|
||||
".button-wrapper[data-image-index='0'] .wrap-image-grid-button"
|
||||
);
|
||||
|
||||
assert.strictEqual(
|
||||
query(".d-editor-input").value,
|
||||
uploads.join("\n"),
|
||||
"Image grid toggles off"
|
||||
);
|
||||
|
||||
const multipleImages = `![zorro|10x10](upload://zorro.png) ![z2|20x20](upload://zorrito.png)\nand a second group of images\n\n${uploads.join(
|
||||
"\n"
|
||||
)}`;
|
||||
await fillIn(".d-editor-input", multipleImages);
|
||||
|
||||
await click(".image-wrapper:first-child .wrap-image-grid-button");
|
||||
|
||||
assert.strictEqual(
|
||||
query(".d-editor-input").value,
|
||||
`[grid]![zorro|10x10](upload://zorro.png) ![z2|20x20](upload://zorrito.png)[/grid]
|
||||
and a second group of images
|
||||
|
||||
![image_example_0|666x500](upload://q4iRxcuSAzfnbUaCsbjMXcGrpaK.jpeg)
|
||||
![image_example_1|481x480](upload://p1ijebM2iyQcUswBffKwMny3gxu.jpeg)
|
||||
![image_example_3|481x480](upload://p1ijebM2iyQcUswBffKwMny3gxu.jpeg)`,
|
||||
"First image grid toggles on"
|
||||
);
|
||||
|
||||
await click(".image-wrapper:nth-of-type(1) .wrap-image-grid-button");
|
||||
|
||||
assert.strictEqual(
|
||||
query(".d-editor-input").value,
|
||||
multipleImages,
|
||||
"First image grid toggles off"
|
||||
);
|
||||
|
||||
// Second group of images is in paragraph 2
|
||||
assert.ok(
|
||||
query(
|
||||
".d-editor-preview p:nth-child(2) .wrap-image-grid-button[data-image-count='3']"
|
||||
),
|
||||
"Grid button has correct image count"
|
||||
);
|
||||
|
||||
await click(".d-editor-preview p:nth-child(2) .wrap-image-grid-button");
|
||||
|
||||
assert.strictEqual(
|
||||
query(".d-editor-input").value,
|
||||
`![zorro|10x10](upload://zorro.png) ![z2|20x20](upload://zorrito.png)
|
||||
and a second group of images
|
||||
|
||||
[grid]
|
||||
![image_example_0|666x500](upload://q4iRxcuSAzfnbUaCsbjMXcGrpaK.jpeg)
|
||||
![image_example_1|481x480](upload://p1ijebM2iyQcUswBffKwMny3gxu.jpeg)
|
||||
![image_example_3|481x480](upload://p1ijebM2iyQcUswBffKwMny3gxu.jpeg)
|
||||
[/grid]`,
|
||||
"Second image grid toggles on"
|
||||
);
|
||||
});
|
||||
|
||||
test("Image Grid Preview", async function (assert) {
|
||||
await visit("/");
|
||||
|
||||
const uploads = [
|
||||
"![image_example_0|666x500](upload://q4iRxcuSAzfnbUaCsbjMXcGrpaK.jpeg)",
|
||||
"![image_example_1|481x480](upload://p1ijebM2iyQcUswBffKwMny3gxu.jpeg)",
|
||||
];
|
||||
|
||||
await click("#create-topic");
|
||||
await fillIn(".d-editor-input", uploads.join("\n"));
|
||||
|
||||
assert.ok(
|
||||
query(
|
||||
".image-wrapper:first-child .wrap-image-grid-button[data-image-count='2']"
|
||||
),
|
||||
"Grid button has correct image count"
|
||||
);
|
||||
|
||||
await click(
|
||||
".button-wrapper[data-image-index='0'] .wrap-image-grid-button"
|
||||
);
|
||||
|
||||
assert.strictEqual(
|
||||
document.querySelectorAll(".d-editor-preview .d-image-grid-column")
|
||||
.length,
|
||||
2,
|
||||
"Preview organizes images into two columns"
|
||||
);
|
||||
|
||||
await fillIn(".d-editor-input", `[grid]\n${uploads[0]}\n[/grid]`);
|
||||
|
||||
assert.ok(
|
||||
query(".d-editor-preview .d-image-grid[data-disabled]"),
|
||||
"Grid is disabled when there is only one image"
|
||||
);
|
||||
|
||||
await fillIn(
|
||||
".d-editor-input",
|
||||
`[grid]${uploads[0]} ${uploads[1]} ${uploads[0]} ${uploads[1]}[/grid]`
|
||||
);
|
||||
|
||||
assert.ok(
|
||||
document.querySelectorAll(".d-editor-preview .d-image-grid-column")
|
||||
.length,
|
||||
2,
|
||||
"Special case of two columns for 4 images"
|
||||
);
|
||||
});
|
||||
});
|
|
@ -1745,4 +1745,60 @@ var bar = 'bar';
|
|||
"code block with html alias work"
|
||||
);
|
||||
});
|
||||
|
||||
test("image grid", function (assert) {
|
||||
assert.cooked(
|
||||
"[grid]\n![](http://folksy.com/images/folksy-colour.png)\n[/grid]",
|
||||
`<p>[grid]<br>
|
||||
<img src="http://folksy.com/images/folksy-colour.png" alt role="presentation"><br>
|
||||
[/grid]</p>`,
|
||||
"image grid without site setting does not work"
|
||||
);
|
||||
|
||||
assert.cookedOptions(
|
||||
"[grid]\n![](http://folksy.com/images/folksy-colour.png)\n[/grid]",
|
||||
{ siteSettings: { experimental_post_image_grid: true } },
|
||||
`<div class="d-image-grid">
|
||||
<p><img src="http://folksy.com/images/folksy-colour.png" alt role="presentation"></p>
|
||||
</div>`,
|
||||
"image grid with site setting works"
|
||||
);
|
||||
|
||||
assert.cookedOptions(
|
||||
`[grid]
|
||||
![](http://folksy.com/images/folksy-colour.png)
|
||||
![](http://folksy.com/images/folksy-colour2.png)
|
||||
![](http://folksy.com/images/folksy-colour3.png)
|
||||
[/grid]`,
|
||||
{ siteSettings: { experimental_post_image_grid: true } },
|
||||
`<div class="d-image-grid">
|
||||
<p><img src="http://folksy.com/images/folksy-colour.png" alt role="presentation"><br>
|
||||
<img src="http://folksy.com/images/folksy-colour2.png" alt role="presentation"><br>
|
||||
<img src="http://folksy.com/images/folksy-colour3.png" alt role="presentation"></p>
|
||||
</div>`,
|
||||
"image grid with 3 images works"
|
||||
);
|
||||
|
||||
assert.cookedOptions(
|
||||
`[grid]
|
||||
![](http://folksy.com/images/folksy-colour.png) ![](http://folksy.com/images/folksy-colour2.png)
|
||||
![](http://folksy.com/images/folksy-colour3.png)
|
||||
[/grid]`,
|
||||
{ siteSettings: { experimental_post_image_grid: true } },
|
||||
`<div class="d-image-grid">
|
||||
<p><img src="http://folksy.com/images/folksy-colour.png" alt role="presentation"> <img src="http://folksy.com/images/folksy-colour2.png" alt role="presentation"><br>
|
||||
<img src="http://folksy.com/images/folksy-colour3.png" alt role="presentation"></p>
|
||||
</div>`,
|
||||
"image grid with mixed block and inline images works"
|
||||
);
|
||||
|
||||
assert.cookedOptions(
|
||||
"[grid]![](http://folksy.com/images/folksy-colour.png) ![](http://folksy.com/images/folksy-colour2.png)[/grid]",
|
||||
{ siteSettings: { experimental_post_image_grid: true } },
|
||||
`<div class="d-image-grid">
|
||||
<p><img src="http://folksy.com/images/folksy-colour.png" alt role="presentation"> <img src="http://folksy.com/images/folksy-colour2.png" alt role="presentation"></p>
|
||||
</div>`,
|
||||
"image grid with inline images works"
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -106,6 +106,19 @@ function buildImageDeleteButton() {
|
|||
</span>
|
||||
`;
|
||||
}
|
||||
|
||||
function buildImageGalleryControl(imageCount) {
|
||||
return `
|
||||
<span class="wrap-image-grid-button" title="${I18n.t(
|
||||
"composer.toggle_image_grid"
|
||||
)}" data-image-count="${imageCount}">
|
||||
<svg class="fa d-icon d-icon-th svg-icon svg-string" xmlns="http://www.w3.org/2000/svg">
|
||||
<use href="#th"></use>
|
||||
</svg>
|
||||
</span>
|
||||
`;
|
||||
}
|
||||
|
||||
// We need this to load after `upload-protocol` which is priority 0
|
||||
export const priority = 1;
|
||||
|
||||
|
@ -124,6 +137,12 @@ function ruleWithImageControls(oldRule) {
|
|||
result += oldRule(tokens, idx, options, env, slf);
|
||||
|
||||
result += `<span class="button-wrapper" data-image-index="${index}">`;
|
||||
if (idx === 0) {
|
||||
const imageCount = tokens.filter((x) => x.type === "image").length;
|
||||
if (imageCount > 1) {
|
||||
result += buildImageGalleryControl(imageCount);
|
||||
}
|
||||
}
|
||||
result += buildImageShowAltTextControls(
|
||||
token.attrs[token.attrIndex("alt")][1]
|
||||
);
|
||||
|
@ -181,6 +200,11 @@ export function setup(helper) {
|
|||
"svg[class=fa d-icon d-icon-times svg-icon svg-string]",
|
||||
"svg[class=fa d-icon d-icon-trash-alt svg-icon svg-string]",
|
||||
"use[href=#times]",
|
||||
|
||||
"span.wrap-image-grid-button",
|
||||
"span.wrap-image-grid-button[data-image-count]",
|
||||
"svg[class=fa d-icon d-icon-th svg-icon svg-string]",
|
||||
"use[href=#th]",
|
||||
]);
|
||||
|
||||
helper.registerPlugin((md) => {
|
||||
|
|
|
@ -0,0 +1,27 @@
|
|||
const gridRule = {
|
||||
tag: "grid",
|
||||
before(state) {
|
||||
let token = state.push("bbcode_open", "div", 1);
|
||||
token.attrs = [["class", "d-image-grid"]];
|
||||
},
|
||||
|
||||
after(state) {
|
||||
state.push("bbcode_close", "div", -1);
|
||||
},
|
||||
};
|
||||
|
||||
export function setup(helper) {
|
||||
helper.registerOptions((opts, siteSettings) => {
|
||||
opts.enableGrid = !!siteSettings.experimental_post_image_grid;
|
||||
});
|
||||
|
||||
helper.allowList(["div.d-image-grid"]);
|
||||
|
||||
helper.registerPlugin((md) => {
|
||||
if (!md.options.discourse.enableGrid) {
|
||||
return;
|
||||
}
|
||||
|
||||
md.block.bbcode.ruler.push("grid", gridRule);
|
||||
});
|
||||
}
|
|
@ -10,6 +10,7 @@
|
|||
@import "compose";
|
||||
@import "composer-user-selector";
|
||||
@import "crawler_layout";
|
||||
@import "d-image-grid";
|
||||
@import "d-icon";
|
||||
@import "d-popover";
|
||||
@import "dialog";
|
||||
|
|
66
app/assets/stylesheets/common/base/d-image-grid.scss
Normal file
66
app/assets/stylesheets/common/base/d-image-grid.scss
Normal file
|
@ -0,0 +1,66 @@
|
|||
.d-image-grid:not([data-disabled]) {
|
||||
$grid-column-gap: 6px;
|
||||
|
||||
&[data-columns] {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
&[data-columns="2"] > * {
|
||||
flex-basis: calc(50% - ($grid-column-gap / 2));
|
||||
margin-right: $grid-column-gap;
|
||||
}
|
||||
|
||||
&[data-columns="3"] > * {
|
||||
flex-basis: calc(33.33% - ($grid-column-gap * 0.667));
|
||||
margin-right: $grid-column-gap;
|
||||
}
|
||||
|
||||
.d-image-grid-column {
|
||||
box-sizing: border-box;
|
||||
|
||||
&:last-child {
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
> img {
|
||||
margin-bottom: $grid-column-gap;
|
||||
}
|
||||
|
||||
// Forces images in the grid to fill each column
|
||||
img,
|
||||
> .lightbox-wrapper,
|
||||
> .lightbox-wrapper > .lightbox {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.lightbox-wrapper {
|
||||
.meta .informations {
|
||||
display: none;
|
||||
}
|
||||
.meta .filename {
|
||||
flex-grow: 3;
|
||||
}
|
||||
}
|
||||
|
||||
// when staging edits
|
||||
.image-wrapper {
|
||||
display: block;
|
||||
padding-bottom: $grid-column-gap;
|
||||
margin-bottom: 0em;
|
||||
}
|
||||
}
|
||||
|
||||
.desktop-view .d-editor-preview & {
|
||||
.image-wrapper {
|
||||
padding-bottom: $grid-column-gap;
|
||||
margin-bottom: 0em;
|
||||
.button-wrapper {
|
||||
.scale-btn-container,
|
||||
&[editing] .wrap-image-grid-button {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -183,11 +183,12 @@
|
|||
width: 100%;
|
||||
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
flex-wrap: nowrap;
|
||||
align-items: center;
|
||||
gap: 0 0.5em;
|
||||
|
||||
position: absolute;
|
||||
height: var(--resizer-height);
|
||||
height: calc(var(--resizer-height) + 0.5em);
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
opacity: 0;
|
||||
|
@ -234,15 +235,15 @@
|
|||
}
|
||||
|
||||
.alt-text-readonly-container {
|
||||
flex: 1 1;
|
||||
width: 100%;
|
||||
flex: 1 1 auto;
|
||||
// arbitrary min-width value allows for correct shrinking
|
||||
min-width: 100px;
|
||||
|
||||
.alt-text {
|
||||
margin-right: 0.5em;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.alt-text-edit-btn {
|
||||
|
@ -256,9 +257,9 @@
|
|||
}
|
||||
|
||||
.alt-text-edit-container {
|
||||
margin-top: 0.25em;
|
||||
gap: 0 0.25em;
|
||||
flex: 1;
|
||||
max-width: 100%;
|
||||
|
||||
.alt-text-input,
|
||||
.alt-text-edit-ok,
|
||||
|
@ -267,11 +268,13 @@
|
|||
}
|
||||
|
||||
.alt-text-input {
|
||||
display: inline-flex;
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
margin: 0;
|
||||
padding-left: 0.25em;
|
||||
}
|
||||
|
||||
.alt-text-edit-ok,
|
||||
|
@ -294,6 +297,11 @@
|
|||
}
|
||||
}
|
||||
|
||||
.wrap-image-grid-button {
|
||||
cursor: pointer;
|
||||
color: var(--tertiary);
|
||||
}
|
||||
|
||||
svg {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
|
|
@ -2525,6 +2525,7 @@ en:
|
|||
aria_label: Alt text for image
|
||||
|
||||
delete_image_button: Delete Image
|
||||
toggle_image_grid: Toggle image grid
|
||||
|
||||
notifications:
|
||||
tooltip:
|
||||
|
|
|
@ -988,6 +988,9 @@ posting:
|
|||
autohighlight_all_code:
|
||||
client: true
|
||||
default: false
|
||||
experimental_post_image_grid:
|
||||
client: true
|
||||
default: false
|
||||
highlighted_languages:
|
||||
default: "bash|c|cpp|csharp|css|diff|go|graphql|ini|java|javascript|json|kotlin|lua|makefile|markdown|objectivec|perl|php|php-template|plaintext|python|python-repl|r|ruby|rust|scss|shell|sql|swift|typescript|xml|yaml|wasm"
|
||||
choices: "HighlightJs.languages"
|
||||
|
|
|
@ -203,6 +203,7 @@ module SvgSprite
|
|||
tag
|
||||
tags
|
||||
tasks
|
||||
th
|
||||
thermometer-three-quarters
|
||||
thumbs-down
|
||||
thumbs-up
|
||||
|
|
Loading…
Reference in New Issue
Block a user