FEATURE: image resizing discoverability (#6804)

FEATURE: image resizing discoverability (#6804)

diff --git a/app/assets/javascripts/discourse/components/composer-editor.js.es6 b/app/assets/javascripts/discourse/components/composer-editor.js.es6
index d5e0c8f..c15bd51 100644
--- a/app/assets/javascripts/discourse/components/composer-editor.js.es6
+++ b/app/assets/javascripts/discourse/components/composer-editor.js.es6
@@ -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);
     }
diff --git a/app/assets/javascripts/discourse/components/d-editor.js.es6 b/app/assets/javascripts/discourse/components/d-editor.js.es6
index e100688..4b67908 100644
--- a/app/assets/javascripts/discourse/components/d-editor.js.es6
+++ b/app/assets/javascripts/discourse/components/d-editor.js.es6
@@ -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.
diff --git a/app/assets/javascripts/pretty-text/engines/discourse-markdown-it.js.es6 b/app/assets/javascripts/pretty-text/engines/discourse-markdown-it.js.es6
index 9708f5a..4e503d0 100644
--- a/app/assets/javascripts/pretty-text/engines/discourse-markdown-it.js.es6
+++ b/app/assets/javascripts/pretty-text/engines/discourse-markdown-it.js.es6
@@ -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 &&

[... diff too long, it was truncated ...]

GitHub sha: 7d2ea2d4

1 Like