diff --git a/app/assets/javascripts/discourse-markdown-it/src/features/image-controls.js b/app/assets/javascripts/discourse-markdown-it/src/features/image-controls.js index d6bb2bd1fe7..b63a165f795 100644 --- a/app/assets/javascripts/discourse-markdown-it/src/features/image-controls.js +++ b/app/assets/javascripts/discourse-markdown-it/src/features/image-controls.js @@ -2,6 +2,30 @@ import I18n from "discourse-i18n"; const SCALES = ["100", "75", "50"]; +let apiExtraButton = []; +let apiExtraButtonAllowList = []; + +export function addImageWrapperButton(label, btnClass, icon = null) { + const markup = []; + markup.push(``); + if (icon) { + markup.push(` + + + + `); + } + markup.push(label); + markup.push(""); + + apiExtraButton.push(markup.join("")); + apiExtraButtonAllowList.push(`span.${btnClass}`); + apiExtraButtonAllowList.push( + `svg[class=fa d-icon d-icon-${icon} svg-icon svg-string]` + ); + apiExtraButtonAllowList.push(`use[href=#${icon}]`); +} + function isUpload(token) { return token.content.includes("upload://"); } @@ -157,6 +181,8 @@ function ruleWithImageControls(oldRule) { result += ``; result += buildImageDeleteButton(); + result += apiExtraButton.join(""); + result += ""; return result; @@ -205,6 +231,8 @@ export function setup(helper) { "span.wrap-image-grid-button[data-image-count]", "svg[class=fa d-icon d-icon-th svg-icon svg-string]", "use[href=#th]", + + ...apiExtraButtonAllowList, ]); helper.registerPlugin((md) => { diff --git a/app/assets/javascripts/discourse/app/components/composer-editor.js b/app/assets/javascripts/discourse/app/components/composer-editor.js index 41918025cc5..9c27f80aaf9 100644 --- a/app/assets/javascripts/discourse/app/components/composer-editor.js +++ b/app/assets/javascripts/discourse/app/components/composer-editor.js @@ -101,6 +101,12 @@ export function addComposerUploadMarkdownResolver(resolver) { export function cleanUpComposerUploadMarkdownResolver() { uploadMarkdownResolvers = []; } + +let apiImageWrapperBtnEvents = []; +export function addApiImageWrapperButtonClickEvent(fn) { + apiImageWrapperBtnEvents.push(fn); +} + export default Component.extend(ComposerUploadUppy, { classNameBindings: ["showToolbar:toolbar-visible", ":wmd-controls"], @@ -773,6 +779,12 @@ export default Component.extend(ComposerUploadUppy, { preview.addEventListener("click", this._handleImageDeleteButtonClick); preview.addEventListener("keypress", this._handleAltTextInputKeypress); preview.addEventListener("click", this._handleImageGridButtonClick); + + if (apiImageWrapperBtnEvents.length > 0) { + apiImageWrapperBtnEvents.forEach((fn) => { + preview.addEventListener("click", fn); + }); + } }, @on("willDestroyElement") @@ -802,6 +814,12 @@ export default Component.extend(ComposerUploadUppy, { preview?.removeEventListener("click", this._handleImageGridButtonClick); preview?.removeEventListener("click", this._handleAltTextCancelButtonClick); preview?.removeEventListener("keypress", this._handleAltTextInputKeypress); + + if (apiImageWrapperBtnEvents.length > 0) { + apiImageWrapperBtnEvents.forEach((fn) => { + preview?.removeEventListener("click", fn); + }); + } }, onExpandPopupMenuOptions(toolbarEvent) { diff --git a/app/assets/javascripts/discourse/app/lib/plugin-api.js b/app/assets/javascripts/discourse/app/lib/plugin-api.js index ca592cb76f3..715aee9c39f 100644 --- a/app/assets/javascripts/discourse/app/lib/plugin-api.js +++ b/app/assets/javascripts/discourse/app/lib/plugin-api.js @@ -1,6 +1,7 @@ import $ from "jquery"; import { h } from "virtual-dom"; import { + addApiImageWrapperButtonClickEvent, addComposerUploadHandler, addComposerUploadMarkdownResolver, addComposerUploadPreProcessor, @@ -132,6 +133,7 @@ import { registerIconRenderer, replaceIcon, } from "discourse-common/lib/icon-library"; +import { addImageWrapperButton } from "discourse-markdown-it/features/image-controls"; import { CUSTOM_USER_SEARCH_OPTIONS } from "select-kit/components/user-chooser"; import { modifySelectKit } from "select-kit/mixins/plugin-api"; @@ -140,7 +142,7 @@ import { modifySelectKit } from "select-kit/mixins/plugin-api"; // docs/CHANGELOG-JAVASCRIPT-PLUGIN-API.md whenever you change the version // using the format described at https://keepachangelog.com/en/1.0.0/. -export const PLUGIN_API_VERSION = "1.24.0"; +export const PLUGIN_API_VERSION = "1.25.0"; // This helper prevents us from applying the same `modifyClass` over and over in test mode. function canModify(klass, type, resolverName, changes) { @@ -2687,6 +2689,26 @@ class PluginApi { .lookup("service:admin-custom-user-fields") .addProperty(userFieldProperty); } + + /** + * Adds a custom button to the composer preview's image wrapper + * + * + * ``` + * api.addComposerImageWrapperButton( + * "My Custom Button", + * "custom-button-class" + * "lock" + * (event) => { console.log("Custom button clicked", event) + * }); + * + * ``` + * + */ + addComposerImageWrapperButton(label, btnClass, icon, fn) { + addImageWrapperButton(label, btnClass, icon); + addApiImageWrapperButtonClickEvent(fn); + } } // from http://stackoverflow.com/questions/6832596/how-to-compare-software-version-number-using-js-only-number diff --git a/app/assets/javascripts/discourse/tests/acceptance/composer-image-preview-test.js b/app/assets/javascripts/discourse/tests/acceptance/composer-image-preview-test.js index 3be52a4ba90..ca591126352 100644 --- a/app/assets/javascripts/discourse/tests/acceptance/composer-image-preview-test.js +++ b/app/assets/javascripts/discourse/tests/acceptance/composer-image-preview-test.js @@ -1,5 +1,6 @@ import { click, fillIn, triggerKeyEvent, visit } from "@ember/test-helpers"; import { test } from "qunit"; +import { withPluginApi } from "discourse/lib/plugin-api"; import { acceptance, count, @@ -376,3 +377,53 @@ acceptance("Composer - Image Preview", function (needs) { ); }); }); + +acceptance("Composer - Image Preview - Plugin API", function (needs) { + needs.user({}); + needs.settings({ allow_uncategorized_topics: true }); + needs.site({ can_tag_topics: true }); + needs.pretender((server, helper) => { + server.post("/uploads/lookup-urls", () => { + return helper.response([]); + }); + }); + + needs.hooks.beforeEach(() => { + withPluginApi("1.25.0", (api) => { + api.addComposerImageWrapperButton( + "My Custom Button", + "custom-button-class", + "lock", + (event) => { + if (event.target.classList.contains("custom-button-class")) { + document.querySelector(".d-editor-input").value = + "custom button change"; + } + } + ); + }); + }); + + test("image wrapper includes extra API button and is functional", async function (assert) { + await visit("/"); + await click("#create-topic"); + + await fillIn( + ".d-editor-input", + "![image_example_0|666x500](upload://q4iRxcuSAzfnbUaCsbjMXcGrpaK.jpeg)" + ); + + assert.ok( + exists(".image-wrapper .custom-button-class"), + "The custom button is added to the image preview wrapper" + ); + + await click(".custom-button-class"); + + assert.strictEqual( + query(".d-editor-input").value, + "custom button change", + "The custom button changes the editor input" + ); + }); +}); diff --git a/docs/CHANGELOG-JAVASCRIPT-PLUGIN-API.md b/docs/CHANGELOG-JAVASCRIPT-PLUGIN-API.md index f0382b6803c..9e826ff51e6 100644 --- a/docs/CHANGELOG-JAVASCRIPT-PLUGIN-API.md +++ b/docs/CHANGELOG-JAVASCRIPT-PLUGIN-API.md @@ -7,6 +7,10 @@ in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [1.25.0] - 2024-02-05 + +- Added `addComposerImageWrapperButton` which is used to add a custom button to the composer preview's image wrapper that appears on hover of an uploaded image. + ## [1.24.0] - 2024-01-08 - Added `addAdminSidebarSectionLink` which is used to add a link to a specific admin sidebar section, as a replacement for the `admin-menu` plugin outlet. This only has an effect if the `admin_sidebar_enabled_groups` site setting is in use, which enables the new admin nav sidebar.