FEATURE: image resizing discoverability (#6804)

This commit is contained in:
Maja Komel 2019-02-27 11:46:16 +01:00 committed by Régis Hanol
parent f68a7a16a4
commit 7d2ea2d4dd
6 changed files with 279 additions and 6 deletions

View File

@ -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);
}

View File

@ -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.

View File

@ -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"]);
}
}
}

View File

@ -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);

View File

@ -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;
}

View File

@ -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);
});