REFACTOR: Attach resize controls to images from the markdown pipeline (#8314)
This commit is contained in:
parent
7711df40e6
commit
1c179177e7
|
@ -770,54 +770,20 @@ export default 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>`;
|
||||
_registerImageScaleButtonClick($preview) {
|
||||
// original string `![image|690x220, 50%](upload://1TjaobgKObzpU7xRMw2HuUc87vO.png "image title")`
|
||||
// group 1 `image`
|
||||
// group 2 `690x220`
|
||||
// group 3 `, 50%`
|
||||
// group 4 'upload://1TjaobgKObzpU7xRMw2HuUc87vO.png'
|
||||
// group 4 'upload://1TjaobgKObzpU7xRMw2HuUc87vO.png "image title"'
|
||||
|
||||
$images.each((i, e) => {
|
||||
const $e = $(e);
|
||||
// Notes:
|
||||
// Group 3 is optional. group 4 can match images with or without a markdown title.
|
||||
// All matches are whitespace tolerant as long it's still valid markdown.
|
||||
// If the image is inside a code block, we'll ignore it `(?!(.*`))`.
|
||||
const imageScaleRegex = /!\[(.*?)\|(\d{1,4}x\d{1,4})(,\s*\d{1,3}%)?\]\((upload:\/\/.*?)\)(?!(.*`))/g;
|
||||
|
||||
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)
|
||||
|
@ -852,45 +818,6 @@ export default Component.extend({
|
|||
});
|
||||
},
|
||||
|
||||
_placeImageScaleButtons($preview) {
|
||||
// regex matches only upload placeholders with size defined,
|
||||
// which is required for resizing
|
||||
|
||||
// original string `![image|690x220, 50%](upload://1TjaobgKObzpU7xRMw2HuUc87vO.png "image title")`
|
||||
// group 1 `image`
|
||||
// group 2 `690x220`
|
||||
// group 3 `, 50%`
|
||||
// group 4 'upload://1TjaobgKObzpU7xRMw2HuUc87vO.png'
|
||||
// group 4 'upload://1TjaobgKObzpU7xRMw2HuUc87vO.png "image title"'
|
||||
|
||||
// Notes:
|
||||
// Group 3 is optional. group 4 can match images with or without a markdown title.
|
||||
// All matches are whitespace tolerant as long it's still valid markdown
|
||||
|
||||
const imageScaleRegex = /!\[(.*?)\|(\d{1,4}x\d{1,4})(,\s*\d{1,3}%)?\]\((upload:\/\/.*?)\)/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.element)
|
||||
.find(".d-editor-preview *")
|
||||
.contents()
|
||||
.each(function() {
|
||||
if (this.nodeType !== 3) return; // TEXT_NODE
|
||||
const $this = $(this);
|
||||
|
||||
if ($this.text().match(imageScaleRegex)) {
|
||||
$this.wrap("<span class='codeblock-image'></span>");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const $images = $preview.find("img.resizable, span.codeblock-image");
|
||||
|
||||
this._appendImageScaleButtons($images, imageScaleRegex);
|
||||
this._registerImageScaleButtonClick($preview, imageScaleRegex);
|
||||
},
|
||||
|
||||
@on("willDestroyElement")
|
||||
_unbindUploadTarget() {
|
||||
this._validUploads = 0;
|
||||
|
@ -1079,7 +1006,7 @@ export default Component.extend({
|
|||
);
|
||||
}
|
||||
|
||||
this._placeImageScaleButtons($preview);
|
||||
this._registerImageScaleButtonClick($preview);
|
||||
|
||||
this.trigger("previewRefreshed", $preview);
|
||||
this.afterRefresh($preview);
|
||||
|
|
|
@ -16,4 +16,5 @@
|
|||
//= require ./pretty-text/engines/discourse-markdown/text-post-process
|
||||
//= require ./pretty-text/engines/discourse-markdown/upload-protocol
|
||||
//= require ./pretty-text/engines/discourse-markdown/inject-line-number
|
||||
//= require ./pretty-text/engines/discourse-markdown/resize-controls
|
||||
//= require ./pretty-text/engines/discourse-markdown/d-wrap
|
||||
|
|
|
@ -0,0 +1,157 @@
|
|||
function isUpload(token) {
|
||||
return token.content.includes("upload://");
|
||||
}
|
||||
|
||||
function hasMetadata(token) {
|
||||
return token.content.match(/(\d{1,4}x\d{1,4})/);
|
||||
}
|
||||
|
||||
function buildToken(state, type, tag, klass, nesting) {
|
||||
const token = new state.Token(type, tag, nesting);
|
||||
token.block = true;
|
||||
token.attrs = [["class", klass]];
|
||||
return token;
|
||||
}
|
||||
|
||||
function wrapImage(tokens, index, state, imgNumber) {
|
||||
const imgToken = tokens[index];
|
||||
const selectedScale = imgToken.content
|
||||
.split(",")
|
||||
.pop()
|
||||
.trim();
|
||||
tokens.splice(
|
||||
index,
|
||||
0,
|
||||
buildToken(state, "wrap_image_open", "div", "image-wrapper", 1)
|
||||
);
|
||||
|
||||
const newElements = [];
|
||||
const btnWrapper = buildToken(
|
||||
state,
|
||||
"wrap_button_open",
|
||||
"div",
|
||||
"button-wrapper",
|
||||
1
|
||||
);
|
||||
btnWrapper.attrs.push(["data-image-index", imgNumber]);
|
||||
newElements.push(btnWrapper);
|
||||
|
||||
const minimumScale = 50;
|
||||
const scales = [100, 75, minimumScale];
|
||||
scales.forEach(scale => {
|
||||
const scaleText = `${scale}%`;
|
||||
|
||||
const btnClass =
|
||||
scaleText === selectedScale ? "scale-btn active" : "scale-btn";
|
||||
const scaleBtn = buildToken(
|
||||
state,
|
||||
"scale_button_open",
|
||||
"span",
|
||||
btnClass,
|
||||
1
|
||||
);
|
||||
scaleBtn.attrs.push(["data-scale", scale]);
|
||||
newElements.push(scaleBtn);
|
||||
|
||||
let textToken = buildToken(state, "text", "", "", 0);
|
||||
textToken.content = scaleText;
|
||||
newElements.push(textToken);
|
||||
|
||||
newElements.push(buildToken(state, "scale_button_close", "span", "", -1));
|
||||
|
||||
if (scale !== minimumScale) {
|
||||
newElements.push(buildToken(state, "separator", "span", "separator", 1));
|
||||
let separatorToken = buildToken(state, "text", "", "", 0);
|
||||
separatorToken.content = " • ";
|
||||
newElements.push(separatorToken);
|
||||
newElements.push(buildToken(state, "separator_close", "span", "", -1));
|
||||
}
|
||||
});
|
||||
newElements.push(buildToken(state, "wrap_button_close", "div", "", -1));
|
||||
|
||||
newElements.push(buildToken(state, "wrap_image_close", "div", "", -1));
|
||||
|
||||
const afterImageIndex = index + 2;
|
||||
tokens.splice(afterImageIndex, 0, ...newElements);
|
||||
}
|
||||
|
||||
function removeParagraph(tokens, imageIndex) {
|
||||
if (
|
||||
tokens[imageIndex - 1] &&
|
||||
tokens[imageIndex - 1].type === "paragraph_open"
|
||||
)
|
||||
tokens.splice(imageIndex - 1, 1);
|
||||
if (tokens[imageIndex] && tokens[imageIndex].type === "paragraph_close")
|
||||
tokens.splice(imageIndex, 1);
|
||||
}
|
||||
|
||||
function updateIndexes(indexes, name) {
|
||||
indexes[name].push(indexes.current);
|
||||
indexes.current++;
|
||||
}
|
||||
|
||||
function wrapImages(tokens, tokenIndexes, state, imgNumberIndexes) {
|
||||
//We do this in reverse order because it's easier for #wrapImage to manipulate the tokens array.
|
||||
for (let j = tokenIndexes.length - 1; j >= 0; j--) {
|
||||
let index = tokenIndexes[j];
|
||||
removeParagraph(tokens, index);
|
||||
wrapImage(tokens, index, state, imgNumberIndexes.pop());
|
||||
}
|
||||
}
|
||||
|
||||
function rule(state) {
|
||||
let blockIndexes = [];
|
||||
const indexNumbers = { current: 0, blocks: [], childrens: [] };
|
||||
|
||||
for (let i = 0; i < state.tokens.length; i++) {
|
||||
let blockToken = state.tokens[i];
|
||||
const blockTokenImage = blockToken.tag === "img";
|
||||
|
||||
if (blockTokenImage && isUpload(blockToken) && hasMetadata(blockToken)) {
|
||||
blockIndexes.push(i);
|
||||
updateIndexes(indexNumbers, "blocks");
|
||||
}
|
||||
|
||||
if (!blockToken.children) continue;
|
||||
|
||||
const childrenIndexes = [];
|
||||
for (let j = 0; j < blockToken.children.length; j++) {
|
||||
let token = blockToken.children[j];
|
||||
const childrenImage = token.tag === "img";
|
||||
|
||||
if (childrenImage && isUpload(blockToken) && hasMetadata(token)) {
|
||||
removeParagraph(state.tokens, i);
|
||||
childrenIndexes.push(j);
|
||||
updateIndexes(indexNumbers, "childrens");
|
||||
}
|
||||
}
|
||||
|
||||
wrapImages(
|
||||
blockToken.children,
|
||||
childrenIndexes,
|
||||
state,
|
||||
indexNumbers.childrens
|
||||
);
|
||||
}
|
||||
|
||||
wrapImages(state.tokens, blockIndexes, state, indexNumbers.blocks);
|
||||
}
|
||||
|
||||
export function setup(helper) {
|
||||
const opts = helper.getOptions();
|
||||
if (opts.previewing) {
|
||||
helper.whiteList([
|
||||
"div.image-wrapper",
|
||||
"div.button-wrapper",
|
||||
"span[class=scale-btn]",
|
||||
"span[class=scale-btn active]",
|
||||
"span.separator",
|
||||
"span.scale-btn[data-scale]",
|
||||
"span.button-wrapper[data-image-index]"
|
||||
]);
|
||||
|
||||
helper.registerPlugin(md => {
|
||||
md.core.ruler.after("upload-protocol", "resize-controls", rule);
|
||||
});
|
||||
}
|
||||
}
|
|
@ -20,15 +20,12 @@ function rule(state) {
|
|||
addImage(uploads, blockToken);
|
||||
}
|
||||
|
||||
if (!blockToken.children) {
|
||||
continue;
|
||||
}
|
||||
if (!blockToken.children) continue;
|
||||
|
||||
for (let j = 0; j < blockToken.children.length; j++) {
|
||||
let token = blockToken.children[j];
|
||||
if (token.tag === "img" || token.tag === "a") {
|
||||
addImage(uploads, token);
|
||||
}
|
||||
|
||||
if (token.tag === "img" || token.tag === "a") addImage(uploads, token);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -755,34 +755,46 @@ QUnit.test("Image resizing buttons", async assert => {
|
|||
|
||||
// Default
|
||||
uploads[0] = "![test|690x313, 50%](upload://test.png)";
|
||||
await click(find(".button-wrapper .scale-btn[data-scale='50']")[0]);
|
||||
await click(
|
||||
find(".button-wrapper[data-image-index='0'] .scale-btn[data-scale='50']")
|
||||
);
|
||||
assertImageResized(assert, uploads);
|
||||
|
||||
// Targets the correct image if two on the same line
|
||||
uploads[6] =
|
||||
"![onTheSameLine1|200x200, 50%](upload://onTheSameLine1.jpeg) ![onTheSameLine2|250x250](upload://onTheSameLine2.jpeg)";
|
||||
await click(find(".button-wrapper .scale-btn[data-scale='50']")[3]);
|
||||
await click(
|
||||
find(".button-wrapper[data-image-index='3'] .scale-btn[data-scale='50']")
|
||||
);
|
||||
assertImageResized(assert, uploads);
|
||||
|
||||
// Try the other image on the same line
|
||||
uploads[6] =
|
||||
"![onTheSameLine1|200x200, 50%](upload://onTheSameLine1.jpeg) ![onTheSameLine2|250x250, 75%](upload://onTheSameLine2.jpeg)";
|
||||
await click(find(".button-wrapper .scale-btn[data-scale='75']")[4]);
|
||||
await click(
|
||||
find(".button-wrapper[data-image-index='4'] .scale-btn[data-scale='75']")
|
||||
);
|
||||
assertImageResized(assert, uploads);
|
||||
|
||||
// Make sure we target the correct image if there are duplicates
|
||||
uploads[7] = "![identicalImage|300x300, 50%](upload://identicalImage.png)";
|
||||
await click(find(".button-wrapper .scale-btn[data-scale='50']")[5]);
|
||||
await click(
|
||||
find(".button-wrapper[data-image-index='5'] .scale-btn[data-scale='50']")
|
||||
);
|
||||
assertImageResized(assert, uploads);
|
||||
|
||||
// Try the other dupe
|
||||
uploads[8] = "![identicalImage|300x300, 75%](upload://identicalImage.png)";
|
||||
await click(find(".button-wrapper .scale-btn[data-scale='75']")[6]);
|
||||
await click(
|
||||
find(".button-wrapper[data-image-index='6'] .scale-btn[data-scale='75']")
|
||||
);
|
||||
assertImageResized(assert, uploads);
|
||||
|
||||
// Don't mess with image titles
|
||||
uploads[10] = `![image|690x220, 75%](upload://test.png "image title")`;
|
||||
await click(find(".button-wrapper .scale-btn[data-scale='75']")[8]);
|
||||
await click(
|
||||
find(".button-wrapper[data-image-index='8'] .scale-btn[data-scale='75']")
|
||||
);
|
||||
assertImageResized(assert, uploads);
|
||||
|
||||
await fillIn(
|
||||
|
|
Loading…
Reference in New Issue