mirror of
https://github.com/discourse/discourse.git
synced 2024-11-29 12:13:58 -06:00
FEATURE: image resizing discoverability (#6804)
This commit is contained in:
parent
f68a7a16a4
commit
7d2ea2d4dd
@ -192,6 +192,14 @@ export default Ember.Component.extend({
|
||||
);
|
||||
}
|
||||
|
||||
if (!this.site.mobileView) {
|
||||
$preview
|
||||
.off("touchstart mouseenter", "img")
|
||||
.on("touchstart mouseenter", "img", () => {
|
||||
this._placeImageScaleButtons($preview);
|
||||
});
|
||||
}
|
||||
|
||||
// Focus on the body unless we have a title
|
||||
if (!this.get("composer.canEditTitle") && !this.capabilities.isIOS) {
|
||||
this.$(".d-editor-input").putCursorAtEnd();
|
||||
@ -774,6 +782,116 @@ export default Ember.Component.extend({
|
||||
}
|
||||
},
|
||||
|
||||
_appendImageScaleButtons($images, imageScaleRegex) {
|
||||
const buttonScales = [100, 75, 50];
|
||||
const imageWrapperTemplate = `<div class="image-wrapper"></div>`;
|
||||
const buttonWrapperTemplate = `<div class="button-wrapper"></div>`;
|
||||
const scaleButtonTemplate = `<span class="scale-btn"></a>`;
|
||||
|
||||
$images.each((i, e) => {
|
||||
const $e = $(e);
|
||||
|
||||
const matches = this.get("composer.reply").match(imageScaleRegex);
|
||||
|
||||
// ignore previewed upload markdown in codeblock
|
||||
if (!matches || $e.hasClass("codeblock-image")) return;
|
||||
|
||||
if (!$e.parent().hasClass("image-wrapper")) {
|
||||
const match = matches[i];
|
||||
const matchingPlaceholder = imageScaleRegex.exec(match);
|
||||
|
||||
if (!matchingPlaceholder) return;
|
||||
|
||||
const currentScale = matchingPlaceholder[2] || 100;
|
||||
|
||||
$e.data("index", i).wrap(imageWrapperTemplate);
|
||||
$e.parent().append(
|
||||
$(buttonWrapperTemplate).attr("data-image-index", i)
|
||||
);
|
||||
|
||||
buttonScales.forEach((buttonScale, buttonIndex) => {
|
||||
const activeClass =
|
||||
parseInt(currentScale, 10) === buttonScale ? "active" : "";
|
||||
|
||||
const $scaleButton = $(scaleButtonTemplate)
|
||||
.addClass(activeClass)
|
||||
.attr("data-scale", buttonScale)
|
||||
.text(`${buttonScale}%`);
|
||||
|
||||
const $buttonWrapper = $e.parent().find(".button-wrapper");
|
||||
$buttonWrapper.append($scaleButton);
|
||||
|
||||
if (buttonIndex !== buttonScales.length - 1) {
|
||||
$buttonWrapper.append(`<span class="separator"> | </span>`);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
_registerImageScaleButtonClick($preview, imageScaleRegex) {
|
||||
$preview.off("click", ".scale-btn").on("click", ".scale-btn", e => {
|
||||
const index = parseInt(
|
||||
$(e.target)
|
||||
.parent()
|
||||
.attr("data-image-index")
|
||||
);
|
||||
|
||||
const scale = e.target.attributes["data-scale"].value;
|
||||
const matchingPlaceholder = this.get("composer.reply").match(
|
||||
imageScaleRegex
|
||||
);
|
||||
|
||||
if (matchingPlaceholder) {
|
||||
const match = matchingPlaceholder[index];
|
||||
if (!match) {
|
||||
return;
|
||||
}
|
||||
|
||||
const replacement = match.replace(imageScaleRegex, `$1,${scale}%$3`);
|
||||
this.appEvents.trigger(
|
||||
"composer:replace-text",
|
||||
matchingPlaceholder[index],
|
||||
replacement,
|
||||
{ regex: imageScaleRegex, index }
|
||||
);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
_placeImageScaleButtons($preview) {
|
||||
// regex matches only upload placeholders with size defined,
|
||||
// which is required for resizing
|
||||
|
||||
// original string `![28|690x226,5%](upload://ceEfx3vO7bx7Cecv2co1SrnoTpW.png)`
|
||||
// match 1 `![28|690x226`
|
||||
// match 2 `5`
|
||||
// match 3 `](upload://ceEfx3vO7bx7Cecv2co1SrnoTpW.png)`
|
||||
const imageScaleRegex = /(!\[(?:\S*?(?=\|)\|)*?(?:\d{1,6}x\d{1,6})+?)(?:,?(\d{1,3})?%?)?(\]\(upload:\/\/\S*?\))/g;
|
||||
|
||||
// wraps previewed upload markdown in a codeblock in its own class to keep a track
|
||||
// of indexes later on to replace the correct upload placeholder in the composer
|
||||
if ($preview.find(".codeblock-image").length === 0) {
|
||||
this.$(".d-editor-preview *")
|
||||
.contents()
|
||||
.filter(function() {
|
||||
return this.nodeType === 3; // TEXT_NODE
|
||||
})
|
||||
.each(function() {
|
||||
$(this).replaceWith(
|
||||
$(this)
|
||||
.text()
|
||||
.replace(imageScaleRegex, "<span class='codeblock-image'>$&</a>")
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
const $images = $preview.find("img.resizable, span.codeblock-image");
|
||||
|
||||
this._appendImageScaleButtons($images, imageScaleRegex);
|
||||
this._registerImageScaleButtonClick($preview, imageScaleRegex);
|
||||
},
|
||||
|
||||
@on("willDestroyElement")
|
||||
_unbindUploadTarget() {
|
||||
this._validUploads = 0;
|
||||
@ -811,6 +929,12 @@ export default Ember.Component.extend({
|
||||
this.storeToolbarState(toolbarEvent);
|
||||
},
|
||||
|
||||
showPreview() {
|
||||
const $preview = this.$(".d-editor-preview-wrapper");
|
||||
this._placeImageScaleButtons($preview);
|
||||
this.send("togglePreview");
|
||||
},
|
||||
|
||||
actions: {
|
||||
importQuote(toolbarEvent) {
|
||||
this.importQuote(toolbarEvent);
|
||||
@ -859,7 +983,7 @@ export default Ember.Component.extend({
|
||||
group: "mobileExtras",
|
||||
icon: "television",
|
||||
title: "composer.show_preview",
|
||||
sendAction: this.get("togglePreview")
|
||||
sendAction: this.showPreview.bind(this)
|
||||
});
|
||||
}
|
||||
},
|
||||
@ -967,6 +1091,10 @@ export default Ember.Component.extend({
|
||||
);
|
||||
}
|
||||
|
||||
if (this.site.mobileView && $preview.is(":visible")) {
|
||||
this._placeImageScaleButtons($preview);
|
||||
}
|
||||
|
||||
this.trigger("previewRefreshed", $preview);
|
||||
this.afterRefresh($preview);
|
||||
}
|
||||
|
@ -295,8 +295,8 @@ export default Ember.Component.extend({
|
||||
this.appEvents.on("composer:insert-text", (text, options) =>
|
||||
this._addText(this._getSelected(), text, options)
|
||||
);
|
||||
this.appEvents.on("composer:replace-text", (oldVal, newVal) =>
|
||||
this._replaceText(oldVal, newVal)
|
||||
this.appEvents.on("composer:replace-text", (oldVal, newVal, opts) =>
|
||||
this._replaceText(oldVal, newVal, opts)
|
||||
);
|
||||
}
|
||||
this._mouseTrap = mouseTrap;
|
||||
@ -659,7 +659,7 @@ export default Ember.Component.extend({
|
||||
}
|
||||
},
|
||||
|
||||
_replaceText(oldVal, newVal) {
|
||||
_replaceText(oldVal, newVal, opts) {
|
||||
const val = this.get("value");
|
||||
const needleStart = val.indexOf(oldVal);
|
||||
|
||||
@ -677,8 +677,17 @@ export default Ember.Component.extend({
|
||||
replacement: { start: needleStart, end: needleStart + newVal.length }
|
||||
});
|
||||
|
||||
// Replace value (side effect: cursor at the end).
|
||||
this.set("value", val.replace(oldVal, newVal));
|
||||
if (opts && opts.index && opts.regex) {
|
||||
let i = -1;
|
||||
const newValue = val.replace(opts.regex, match => {
|
||||
i++;
|
||||
return i === opts.index ? newVal : match;
|
||||
});
|
||||
this.set("value", newValue);
|
||||
} else {
|
||||
// Replace value (side effect: cursor at the end).
|
||||
this.set("value", val.replace(oldVal, newVal));
|
||||
}
|
||||
|
||||
if ($("textarea.d-editor-input").is(":focus")) {
|
||||
// Restore cursor.
|
||||
|
@ -168,6 +168,13 @@ function renderImage(tokens, idx, options, env, slf) {
|
||||
if (token.attrIndex("height") === -1) {
|
||||
token.attrs.push(["height", height]);
|
||||
}
|
||||
|
||||
if (
|
||||
options.discourse.previewing &&
|
||||
match[6] !== "x" &&
|
||||
match[4] !== "x"
|
||||
)
|
||||
token.attrs.push(["class", "resizable"]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -53,6 +53,8 @@ function rule(state) {
|
||||
}
|
||||
|
||||
export function setup(helper) {
|
||||
const opts = helper.getOptions();
|
||||
if (opts.previewing) helper.whiteList(["img.resizable"]);
|
||||
helper.whiteList(["img[data-orig-src]"]);
|
||||
helper.registerPlugin(md => {
|
||||
md.core.ruler.push("image-protocol", rule);
|
||||
|
@ -202,3 +202,51 @@
|
||||
padding: 10px;
|
||||
border: 1px solid $primary-low;
|
||||
}
|
||||
|
||||
.d-editor-preview img {
|
||||
padding-bottom: 20px;
|
||||
}
|
||||
|
||||
.d-editor-preview .image-wrapper {
|
||||
position: relative;
|
||||
padding-bottom: 20px;
|
||||
|
||||
img {
|
||||
padding-bottom: 0px;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
.button-wrapper {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
.button-wrapper {
|
||||
transition: opacity 0.2s ease-in;
|
||||
opacity: 0;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
width: 30px;
|
||||
bottom: 0px;
|
||||
display: flex;
|
||||
|
||||
.separator {
|
||||
margin: 0 5px;
|
||||
}
|
||||
|
||||
.scale-btn {
|
||||
color: $tertiary;
|
||||
|
||||
&.active {
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.mobile-view .d-editor-preview .image-wrapper .button-wrapper {
|
||||
opacity: 1;
|
||||
}
|
||||
|
@ -10,6 +10,9 @@ acceptance("Composer", {
|
||||
draft_sequence: 42
|
||||
});
|
||||
});
|
||||
server.post("/uploads/lookup-urls", () => {
|
||||
return helper.response([]);
|
||||
});
|
||||
},
|
||||
settings: {
|
||||
enable_whispers: true
|
||||
@ -596,3 +599,79 @@ QUnit.test("Checks for existing draft", async assert => {
|
||||
|
||||
toggleCheckDraftPopup(false);
|
||||
});
|
||||
|
||||
const assertImageResized = (assert, uploads) => {
|
||||
assert.equal(
|
||||
find(".d-editor-input").val(),
|
||||
uploads.join("\n"),
|
||||
"it resizes uploaded image"
|
||||
);
|
||||
};
|
||||
|
||||
QUnit.test("Image resizing buttons", async assert => {
|
||||
await visit("/");
|
||||
await click("#create-topic");
|
||||
|
||||
let uploads = [
|
||||
"![test|690x313](upload://test.png)",
|
||||
"[img]http://example.com/image.jpg[/img]",
|
||||
"![anotherOne|690x463](upload://anotherOne.jpeg)",
|
||||
"![](upload://withoutAltAndSize.jpeg)",
|
||||
"`![test|690x313](upload://test.png)`",
|
||||
"![withoutSize](upload://withoutSize.png)",
|
||||
"<img src='http://someimage.jpg' wight='20' height='20'>",
|
||||
"![onTheSameLine1|200x200](upload://onTheSameLine1.jpeg) ![onTheSameLine2|250x250](upload://onTheSameLine2.jpeg)",
|
||||
"![identicalImage|300x300](upload://identicalImage.png)",
|
||||
"![identicalImage|300x300](upload://identicalImage.png)"
|
||||
];
|
||||
|
||||
await fillIn(".d-editor-input", uploads.join("\n"));
|
||||
|
||||
assert.ok(
|
||||
find(".button-wrapper").length === 0,
|
||||
"it does not append scaling buttons before hovering images"
|
||||
);
|
||||
|
||||
await triggerEvent($(".d-editor-preview img"), "mouseover");
|
||||
|
||||
assert.ok(
|
||||
find(".button-wrapper").length === 6,
|
||||
"it adds correct amount of scaling button groups"
|
||||
);
|
||||
|
||||
uploads[0] = "![test|690x313,50%](upload://test.png)";
|
||||
await click(find(".button-wrapper .scale-btn[data-scale='50']")[0]);
|
||||
assertImageResized(assert, uploads);
|
||||
|
||||
await triggerEvent($(".d-editor-preview img"), "mouseover");
|
||||
|
||||
uploads[2] = "![anotherOne|690x463,75%](upload://anotherOne.jpeg)";
|
||||
await click(find(".button-wrapper .scale-btn[data-scale='75']")[1]);
|
||||
assertImageResized(assert, uploads);
|
||||
|
||||
await triggerEvent($(".d-editor-preview img"), "mouseover");
|
||||
|
||||
uploads[7] =
|
||||
"![onTheSameLine1|200x200,50%](upload://onTheSameLine1.jpeg) ![onTheSameLine2|250x250](upload://onTheSameLine2.jpeg)";
|
||||
await click(find(".button-wrapper .scale-btn[data-scale='50']")[2]);
|
||||
assertImageResized(assert, uploads);
|
||||
|
||||
await triggerEvent($(".d-editor-preview img"), "mouseover");
|
||||
|
||||
uploads[7] =
|
||||
"![onTheSameLine1|200x200,50%](upload://onTheSameLine1.jpeg) ![onTheSameLine2|250x250,75%](upload://onTheSameLine2.jpeg)";
|
||||
await click(find(".button-wrapper .scale-btn[data-scale='75']")[3]);
|
||||
assertImageResized(assert, uploads);
|
||||
|
||||
await triggerEvent($(".d-editor-preview img"), "mouseover");
|
||||
|
||||
uploads[8] = "![identicalImage|300x300,50%](upload://identicalImage.png)";
|
||||
await click(find(".button-wrapper .scale-btn[data-scale='50']")[4]);
|
||||
assertImageResized(assert, uploads);
|
||||
|
||||
await triggerEvent($(".d-editor-preview img"), "mouseover");
|
||||
|
||||
uploads[9] = "![identicalImage|300x300,75%](upload://identicalImage.png)";
|
||||
await click(find(".button-wrapper .scale-btn[data-scale='75']")[5]);
|
||||
assertImageResized(assert, uploads);
|
||||
});
|
||||
|
Loading…
Reference in New Issue
Block a user