mirror of
https://github.com/discourse/discourse.git
synced 2025-01-18 11:52:45 +08: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
|
// Focus on the body unless we have a title
|
||||||
if (!this.get("composer.canEditTitle") && !this.capabilities.isIOS) {
|
if (!this.get("composer.canEditTitle") && !this.capabilities.isIOS) {
|
||||||
this.$(".d-editor-input").putCursorAtEnd();
|
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")
|
@on("willDestroyElement")
|
||||||
_unbindUploadTarget() {
|
_unbindUploadTarget() {
|
||||||
this._validUploads = 0;
|
this._validUploads = 0;
|
||||||
|
@ -811,6 +929,12 @@ export default Ember.Component.extend({
|
||||||
this.storeToolbarState(toolbarEvent);
|
this.storeToolbarState(toolbarEvent);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
showPreview() {
|
||||||
|
const $preview = this.$(".d-editor-preview-wrapper");
|
||||||
|
this._placeImageScaleButtons($preview);
|
||||||
|
this.send("togglePreview");
|
||||||
|
},
|
||||||
|
|
||||||
actions: {
|
actions: {
|
||||||
importQuote(toolbarEvent) {
|
importQuote(toolbarEvent) {
|
||||||
this.importQuote(toolbarEvent);
|
this.importQuote(toolbarEvent);
|
||||||
|
@ -859,7 +983,7 @@ export default Ember.Component.extend({
|
||||||
group: "mobileExtras",
|
group: "mobileExtras",
|
||||||
icon: "television",
|
icon: "television",
|
||||||
title: "composer.show_preview",
|
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.trigger("previewRefreshed", $preview);
|
||||||
this.afterRefresh($preview);
|
this.afterRefresh($preview);
|
||||||
}
|
}
|
||||||
|
|
|
@ -295,8 +295,8 @@ export default Ember.Component.extend({
|
||||||
this.appEvents.on("composer:insert-text", (text, options) =>
|
this.appEvents.on("composer:insert-text", (text, options) =>
|
||||||
this._addText(this._getSelected(), text, options)
|
this._addText(this._getSelected(), text, options)
|
||||||
);
|
);
|
||||||
this.appEvents.on("composer:replace-text", (oldVal, newVal) =>
|
this.appEvents.on("composer:replace-text", (oldVal, newVal, opts) =>
|
||||||
this._replaceText(oldVal, newVal)
|
this._replaceText(oldVal, newVal, opts)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
this._mouseTrap = mouseTrap;
|
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 val = this.get("value");
|
||||||
const needleStart = val.indexOf(oldVal);
|
const needleStart = val.indexOf(oldVal);
|
||||||
|
|
||||||
|
@ -677,8 +677,17 @@ export default Ember.Component.extend({
|
||||||
replacement: { start: needleStart, end: needleStart + newVal.length }
|
replacement: { start: needleStart, end: needleStart + newVal.length }
|
||||||
});
|
});
|
||||||
|
|
||||||
// Replace value (side effect: cursor at the end).
|
if (opts && opts.index && opts.regex) {
|
||||||
this.set("value", val.replace(oldVal, newVal));
|
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")) {
|
if ($("textarea.d-editor-input").is(":focus")) {
|
||||||
// Restore cursor.
|
// Restore cursor.
|
||||||
|
|
|
@ -168,6 +168,13 @@ function renderImage(tokens, idx, options, env, slf) {
|
||||||
if (token.attrIndex("height") === -1) {
|
if (token.attrIndex("height") === -1) {
|
||||||
token.attrs.push(["height", height]);
|
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) {
|
export function setup(helper) {
|
||||||
|
const opts = helper.getOptions();
|
||||||
|
if (opts.previewing) helper.whiteList(["img.resizable"]);
|
||||||
helper.whiteList(["img[data-orig-src]"]);
|
helper.whiteList(["img[data-orig-src]"]);
|
||||||
helper.registerPlugin(md => {
|
helper.registerPlugin(md => {
|
||||||
md.core.ruler.push("image-protocol", rule);
|
md.core.ruler.push("image-protocol", rule);
|
||||||
|
|
|
@ -202,3 +202,51 @@
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
border: 1px solid $primary-low;
|
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
|
draft_sequence: 42
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
server.post("/uploads/lookup-urls", () => {
|
||||||
|
return helper.response([]);
|
||||||
|
});
|
||||||
},
|
},
|
||||||
settings: {
|
settings: {
|
||||||
enable_whispers: true
|
enable_whispers: true
|
||||||
|
@ -596,3 +599,79 @@ QUnit.test("Checks for existing draft", async assert => {
|
||||||
|
|
||||||
toggleCheckDraftPopup(false);
|
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