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) {
|
_registerImageAltTextButtonClick(preview) {
|
||||||
preview.addEventListener("click", this._handleAltTextEditButtonClick);
|
preview.addEventListener("click", this._handleAltTextEditButtonClick);
|
||||||
preview.addEventListener("click", this._handleAltTextOkButtonClick);
|
preview.addEventListener("click", this._handleAltTextOkButtonClick);
|
||||||
preview.addEventListener("click", this._handleAltTextCancelButtonClick);
|
preview.addEventListener("click", this._handleAltTextCancelButtonClick);
|
||||||
preview.addEventListener("click", this._handleImageDeleteButtonClick);
|
preview.addEventListener("click", this._handleImageDeleteButtonClick);
|
||||||
preview.addEventListener("keypress", this._handleAltTextInputKeypress);
|
preview.addEventListener("keypress", this._handleAltTextInputKeypress);
|
||||||
|
if (this.siteSettings.experimental_post_image_grid) {
|
||||||
|
preview.addEventListener("click", this._handleImageGridButtonClick);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
@on("willDestroyElement")
|
@on("willDestroyElement")
|
||||||
|
@ -773,6 +805,10 @@ export default Component.extend(
|
||||||
preview?.removeEventListener("click", this._handleImageScaleButtonClick);
|
preview?.removeEventListener("click", this._handleImageScaleButtonClick);
|
||||||
preview?.removeEventListener("click", this._handleAltTextEditButtonClick);
|
preview?.removeEventListener("click", this._handleAltTextEditButtonClick);
|
||||||
preview?.removeEventListener("click", this._handleAltTextOkButtonClick);
|
preview?.removeEventListener("click", this._handleAltTextOkButtonClick);
|
||||||
|
preview?.removeEventListener("click", this._handleImageDeleteButtonClick);
|
||||||
|
if (this.siteSettings.experimental_post_image_grid) {
|
||||||
|
preview?.removeEventListener("click", this._handleImageGridButtonClick);
|
||||||
|
}
|
||||||
preview?.removeEventListener(
|
preview?.removeEventListener(
|
||||||
"click",
|
"click",
|
||||||
this._handleAltTextCancelButtonClick
|
this._handleAltTextCancelButtonClick
|
||||||
|
|
|
@ -309,6 +309,7 @@ export default Component.extend(TextareaTextManipulation, {
|
||||||
this.appEvents.on("composer:insert-block", this, "insertBlock");
|
this.appEvents.on("composer:insert-block", this, "insertBlock");
|
||||||
this.appEvents.on("composer:insert-text", this, "insertText");
|
this.appEvents.on("composer:insert-text", this, "insertText");
|
||||||
this.appEvents.on("composer:replace-text", this, "replaceText");
|
this.appEvents.on("composer:replace-text", this, "replaceText");
|
||||||
|
this.appEvents.on("composer:apply-surround", this, "_applySurround");
|
||||||
this.appEvents.on(
|
this.appEvents.on(
|
||||||
"composer:indent-selected-text",
|
"composer:indent-selected-text",
|
||||||
this,
|
this,
|
||||||
|
@ -349,6 +350,7 @@ export default Component.extend(TextareaTextManipulation, {
|
||||||
this.appEvents.off("composer:insert-block", this, "insertBlock");
|
this.appEvents.off("composer:insert-block", this, "insertBlock");
|
||||||
this.appEvents.off("composer:insert-text", this, "insertText");
|
this.appEvents.off("composer:insert-text", this, "insertText");
|
||||||
this.appEvents.off("composer:replace-text", this, "replaceText");
|
this.appEvents.off("composer:replace-text", this, "replaceText");
|
||||||
|
this.appEvents.off("composer:apply-surround", this, "_applySurround");
|
||||||
this.appEvents.off(
|
this.appEvents.off(
|
||||||
"composer:indent-selected-text",
|
"composer:indent-selected-text",
|
||||||
this,
|
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() {
|
_toggleDirection() {
|
||||||
let currentDir = this._$textarea.attr("dir")
|
let currentDir = this._$textarea.attr("dir")
|
||||||
? this._$textarea.attr("dir")
|
? this._$textarea.attr("dir")
|
||||||
|
|
|
@ -3,6 +3,7 @@ import discourseLater from "discourse-common/lib/later";
|
||||||
import I18n from "I18n";
|
import I18n from "I18n";
|
||||||
import highlightSyntax from "discourse/lib/highlight-syntax";
|
import highlightSyntax from "discourse/lib/highlight-syntax";
|
||||||
import lightbox from "discourse/lib/lightbox";
|
import lightbox from "discourse/lib/lightbox";
|
||||||
|
import Columns from "discourse/lib/columns";
|
||||||
import { iconHTML, iconNode } from "discourse-common/lib/icon-library";
|
import { iconHTML, iconNode } from "discourse-common/lib/icon-library";
|
||||||
import { setTextDirections } from "discourse/lib/text-direction";
|
import { setTextDirections } from "discourse/lib/text-direction";
|
||||||
import { nativeLazyLoading } from "discourse/lib/lazy-load-images";
|
import { nativeLazyLoading } from "discourse/lib/lazy-load-images";
|
||||||
|
@ -33,6 +34,25 @@ export default {
|
||||||
{ id: "discourse-lightbox" }
|
{ 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) {
|
if (siteSettings.support_mixed_text_direction) {
|
||||||
api.decorateCookedElement(setTextDirections, {
|
api.decorateCookedElement(setTextDirections, {
|
||||||
id: "discourse-text-direction",
|
id: "discourse-text-direction",
|
||||||
|
|
|
@ -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"
|
"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>
|
</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
|
// We need this to load after `upload-protocol` which is priority 0
|
||||||
export const priority = 1;
|
export const priority = 1;
|
||||||
|
|
||||||
|
@ -124,6 +137,12 @@ function ruleWithImageControls(oldRule) {
|
||||||
result += oldRule(tokens, idx, options, env, slf);
|
result += oldRule(tokens, idx, options, env, slf);
|
||||||
|
|
||||||
result += `<span class="button-wrapper" data-image-index="${index}">`;
|
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(
|
result += buildImageShowAltTextControls(
|
||||||
token.attrs[token.attrIndex("alt")][1]
|
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-times svg-icon svg-string]",
|
||||||
"svg[class=fa d-icon d-icon-trash-alt svg-icon svg-string]",
|
"svg[class=fa d-icon d-icon-trash-alt svg-icon svg-string]",
|
||||||
"use[href=#times]",
|
"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) => {
|
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 "compose";
|
||||||
@import "composer-user-selector";
|
@import "composer-user-selector";
|
||||||
@import "crawler_layout";
|
@import "crawler_layout";
|
||||||
|
@import "d-image-grid";
|
||||||
@import "d-icon";
|
@import "d-icon";
|
||||||
@import "d-popover";
|
@import "d-popover";
|
||||||
@import "dialog";
|
@import "dialog";
|
||||||
|
|
|
@ -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%;
|
width: 100%;
|
||||||
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: nowrap;
|
||||||
|
align-items: center;
|
||||||
gap: 0 0.5em;
|
gap: 0 0.5em;
|
||||||
|
|
||||||
position: absolute;
|
position: absolute;
|
||||||
height: var(--resizer-height);
|
height: calc(var(--resizer-height) + 0.5em);
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
|
@ -234,15 +235,15 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.alt-text-readonly-container {
|
.alt-text-readonly-container {
|
||||||
flex: 1 1;
|
flex: 1 1 auto;
|
||||||
width: 100%;
|
// arbitrary min-width value allows for correct shrinking
|
||||||
|
min-width: 100px;
|
||||||
|
|
||||||
.alt-text {
|
.alt-text {
|
||||||
margin-right: 0.5em;
|
margin-right: 0.5em;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
max-width: 100%;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.alt-text-edit-btn {
|
.alt-text-edit-btn {
|
||||||
|
@ -256,9 +257,9 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.alt-text-edit-container {
|
.alt-text-edit-container {
|
||||||
margin-top: 0.25em;
|
|
||||||
gap: 0 0.25em;
|
gap: 0 0.25em;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
|
max-width: 100%;
|
||||||
|
|
||||||
.alt-text-input,
|
.alt-text-input,
|
||||||
.alt-text-edit-ok,
|
.alt-text-edit-ok,
|
||||||
|
@ -267,11 +268,13 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.alt-text-input {
|
.alt-text-input {
|
||||||
|
display: inline-flex;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
padding-left: 0.25em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.alt-text-edit-ok,
|
.alt-text-edit-ok,
|
||||||
|
@ -294,6 +297,11 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.wrap-image-grid-button {
|
||||||
|
cursor: pointer;
|
||||||
|
color: var(--tertiary);
|
||||||
|
}
|
||||||
|
|
||||||
svg {
|
svg {
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
|
|
@ -2525,6 +2525,7 @@ en:
|
||||||
aria_label: Alt text for image
|
aria_label: Alt text for image
|
||||||
|
|
||||||
delete_image_button: Delete Image
|
delete_image_button: Delete Image
|
||||||
|
toggle_image_grid: Toggle image grid
|
||||||
|
|
||||||
notifications:
|
notifications:
|
||||||
tooltip:
|
tooltip:
|
||||||
|
|
|
@ -988,6 +988,9 @@ posting:
|
||||||
autohighlight_all_code:
|
autohighlight_all_code:
|
||||||
client: true
|
client: true
|
||||||
default: false
|
default: false
|
||||||
|
experimental_post_image_grid:
|
||||||
|
client: true
|
||||||
|
default: false
|
||||||
highlighted_languages:
|
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"
|
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"
|
choices: "HighlightJs.languages"
|
||||||
|
|
|
@ -203,6 +203,7 @@ module SvgSprite
|
||||||
tag
|
tag
|
||||||
tags
|
tags
|
||||||
tasks
|
tasks
|
||||||
|
th
|
||||||
thermometer-three-quarters
|
thermometer-three-quarters
|
||||||
thumbs-down
|
thumbs-down
|
||||||
thumbs-up
|
thumbs-up
|
||||||
|
|
Loading…
Reference in New Issue