FEATURE: allow plugins to specify keyboard shortcuts for hidden toolbar items (#28456)

Previous to this change there is no clean way to apply keyboard shortcuts
to things such as "add poll" and other hidden options in the toolbar

This allows shortcuts to be specified similar to how they are on the toolbar



Co-authored-by: Joffrey JAFFEUX <j.jaffeux@gmail.com>
This commit is contained in:
Sam 2024-08-23 09:28:28 +10:00 committed by GitHub
parent 02af4fb5b8
commit 7ab7e6bb23
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 165 additions and 36 deletions

View File

@ -322,6 +322,19 @@ export default Component.extend(TextareaTextManipulation, {
});
});
if (this.popupMenuOptions && this.onPopupMenuAction) {
this.popupMenuOptions.forEach((popupButton) => {
if (popupButton.shortcut && popupButton.condition) {
const shortcut =
`${PLATFORM_KEY_MODIFIER}+${popupButton.shortcut}`.toLowerCase();
this._itsatrap.bind(shortcut, () => {
this.onPopupMenuAction(popupButton, this.newToolbarEvent());
return false;
});
}
});
}
this._itsatrap.bind("tab", () => this.indentSelection("right"));
this._itsatrap.bind("shift+tab", () => this.indentSelection("left"));
this._itsatrap.bind(`${PLATFORM_KEY_MODIFIER}+shift+.`, () =>
@ -785,6 +798,23 @@ export default Component.extend(TextareaTextManipulation, {
}
},
newToolbarEvent(trimLeading) {
const selected = this.getSelected(trimLeading);
return {
selected,
selectText: (from, length) =>
this.selectText(from, length, { scroll: false }),
applySurround: (head, tail, exampleKey, opts) =>
this.applySurround(selected, head, tail, exampleKey, opts),
applyList: (head, exampleKey, opts) =>
this._applyList(selected, head, exampleKey, opts),
formatCode: (...args) => this.send("formatCode", args),
addText: (text) => this.addText(selected, text),
getText: () => this.value,
toggleDirection: () => this._toggleDirection(),
};
},
actions: {
emoji() {
if (this.disabled) {
@ -799,21 +829,7 @@ export default Component.extend(TextareaTextManipulation, {
return;
}
const selected = this.getSelected(button.trimLeading);
const toolbarEvent = {
selected,
selectText: (from, length) =>
this.selectText(from, length, { scroll: false }),
applySurround: (head, tail, exampleKey, opts) =>
this.applySurround(selected, head, tail, exampleKey, opts),
applyList: (head, exampleKey, opts) =>
this._applyList(selected, head, exampleKey, opts),
formatCode: (...args) => this.send("formatCode", args),
addText: (text) => this.addText(selected, text),
getText: () => this.value,
toggleDirection: () => this._toggleDirection(),
};
const toolbarEvent = this.newToolbarEvent(button.trimLeading);
if (button.sendAction) {
return button.sendAction(toolbarEvent);
} else {

View File

@ -3,7 +3,7 @@
// 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.37.0";
export const PLUGIN_API_VERSION = "1.37.1";
import $ from "jquery";
import { h } from "virtual-dom";
@ -960,6 +960,7 @@ class PluginApi {
* @param {Object} opts - An Object.
* @param {string} opts.icon - The name of the FontAwesome icon to display for the button.
* @param {string} opts.label - The I18n translation key for the button's label.
* @param {string} opts.shortcut - The keyboard shortcut to apply, NOTE: this will unconditionally add CTRL/META key (eg: m means CTRL+m).
* @param {action} opts.action - The action to perform when the button is clicked.
* @param {condition} opts.condition - A condition that must be met for the button to be displayed.
*
@ -970,6 +971,7 @@ class PluginApi {
* },
* icon: 'far-bold',
* label: 'composer.bold_some_text',
* shortcut: 'm',
* condition: (composer) => {
* return composer.editingPost;
* }

View File

@ -665,7 +665,7 @@ export default class ComposerService extends Service {
}
@action
onPopupMenuAction(menuItem) {
onPopupMenuAction(menuItem, toolbarEvent) {
// menuItem is an object with keys name & action like so: { name: "toggle-invisible, action: "toggleInvisible" }
// `action` value can either be a string (to lookup action by) or a function to call
this.appEvents.trigger(
@ -673,7 +673,12 @@ export default class ComposerService extends Service {
menuItem
);
if (typeof menuItem.action === "function") {
return menuItem.action(this.toolbarEvent);
// note due to the way args are passed to actions we need
// to treate the explicity toolbarEvent as a fallback for no
// event
// Long term we want to avoid needing this awkwardness and pass
// the event explicitly
return menuItem.action(this.toolbarEvent || toolbarEvent);
} else {
return (
this.actions?.[menuItem.action]?.bind(this) || // Legacy-style contributions from themes/plugins

View File

@ -10,8 +10,10 @@ import {
} from "@ember/test-helpers";
import { test } from "qunit";
import sinon from "sinon";
import { PLATFORM_KEY_MODIFIER } from "discourse/lib/keyboard-shortcuts";
import LinkLookup from "discourse/lib/link-lookup";
import { withPluginApi } from "discourse/lib/plugin-api";
import { translateModKey } from "discourse/lib/utilities";
import Composer, {
CREATE_TOPIC,
NEW_TOPIC_KEY,
@ -618,7 +620,7 @@ acceptance("Composer", function (needs) {
await click(".topic-post:nth-of-type(1) button.reply");
await menu.expand();
await menu.selectRowByName(I18n.t("composer.toggle_whisper"));
await menu.selectRowByName("toggle-whisper");
assert.strictEqual(
count(".composer-actions svg.d-icon-far-eye-slash"),
@ -627,7 +629,7 @@ acceptance("Composer", function (needs) {
);
await menu.expand();
await menu.selectRowByName(I18n.t("composer.toggle_whisper"));
await menu.selectRowByName("toggle-whisper");
assert.ok(
!exists(".composer-actions svg.d-icon-far-eye-slash"),
@ -635,14 +637,14 @@ acceptance("Composer", function (needs) {
);
await menu.expand();
await menu.selectRowByName(I18n.t("composer.toggle_whisper"));
await menu.selectRowByName("toggle-whisper");
await click(".toggle-fullscreen");
await menu.expand();
assert.ok(
menu.rowByName(I18n.t("composer.toggle_whisper")).exists(),
menu.rowByName("toggle-whisper").exists(),
"whisper toggling is still present when going fullscreen"
);
});
@ -732,7 +734,7 @@ acceptance("Composer", function (needs) {
await selectKit(".toolbar-popup-menu-options").expand();
await selectKit(".toolbar-popup-menu-options").selectRowByName(
I18n.t("composer.toggle_whisper")
"toggle-whisper"
);
assert.strictEqual(
@ -752,7 +754,7 @@ acceptance("Composer", function (needs) {
await selectKit(".toolbar-popup-menu-options").expand();
await selectKit(".toolbar-popup-menu-options").selectRowByName(
I18n.t("composer.toggle_unlisted")
"toggle-invisible"
);
assert.ok(
@ -1404,6 +1406,61 @@ acceptance("composer buttons API", function (needs) {
allow_uncategorized_topics: true,
});
test("buttons can support a shortcut", async function (assert) {
withPluginApi("0", (api) => {
api.addComposerToolbarPopupMenuOption({
action: (toolbarEvent) => {
toolbarEvent.applySurround("**", "**");
},
shortcut: "alt+b",
icon: "far-bold",
name: "bold",
title: "some_title",
label: "some_label",
condition: () => {
return true;
},
});
});
await visit("/t/internationalization-localization/280");
await click(".post-controls button.reply");
await fillIn(".d-editor-input", "hello the world");
const editor = document.querySelector(".d-editor-input");
editor.setSelectionRange(6, 9); // select the text input in the composer
await triggerKeyEvent(
".d-editor-input",
"keydown",
"B",
Object.assign({ altKey: true }, metaModifier)
);
assert.strictEqual(editor.value, "hello **the** world", "it adds the bold");
const dropdown = selectKit(".toolbar-popup-menu-options");
await dropdown.expand();
const row = dropdown.rowByName("bold").el();
assert
.dom(row)
.hasAttribute(
"title",
I18n.t("some_title") +
` (${translateModKey(PLATFORM_KEY_MODIFIER + "+alt+b")})`,
"it shows the title with shortcut"
);
assert
.dom(row)
.hasText(
I18n.t("some_label") +
` ${translateModKey(PLATFORM_KEY_MODIFIER + "+alt+b")}`,
"it shows the label with shortcut"
);
});
test("buttons can be added conditionally", async function (assert) {
withPluginApi("0", (api) => {
api.addComposerToolbarPopupMenuOption({

View File

@ -2,7 +2,6 @@ import { click, visit } from "@ember/test-helpers";
import { test } from "qunit";
import { acceptance, exists } from "discourse/tests/helpers/qunit-helpers";
import selectKit from "discourse/tests/helpers/select-kit-helper";
import I18n from "discourse-i18n";
acceptance("Table Builder", function (needs) {
needs.user();
@ -14,7 +13,7 @@ acceptance("Table Builder", function (needs) {
await selectKit(".toolbar-popup-menu-options").expand();
assert
.dom(`.select-kit-row[data-name='${I18n.t("composer.insert_table")}']`)
.dom(`.select-kit-row[data-name='toggle-spreadsheet']`)
.exists("it shows the builder button");
});
@ -27,7 +26,7 @@ acceptance("Table Builder", function (needs) {
await selectKit(".toolbar-popup-menu-options").expand();
assert
.dom(`.select-kit-row[data-name='${I18n.t("composer.insert_table")}']`)
.dom(`.select-kit-row[data-name='toggle-spreadsheet']`)
.exists("it shows the builder button");
});
});

View File

@ -9,5 +9,7 @@
<div class="texts">
<span class="name">{{html-safe this.label}}</span>
<span class="desc">{{html-safe this.description}}</span>
{{#if this.description}}
<span class="desc">{{html-safe this.description}}</span>
{{/if}}
</div>

View File

@ -1,3 +1,5 @@
import { PLATFORM_KEY_MODIFIER } from "discourse/lib/keyboard-shortcuts";
import { translateModKey } from "discourse/lib/utilities";
import I18n from "discourse-i18n";
import DropdownSelectBoxComponent from "select-kit/components/dropdown-select-box";
@ -17,9 +19,36 @@ export default DropdownSelectBoxComponent.extend({
return contents
.map((content) => {
if (content.condition) {
let label;
if (content.label) {
label = I18n.t(content.label);
if (content.shortcut) {
label += ` <kbd class="shortcut">${translateModKey(
PLATFORM_KEY_MODIFIER
)}+${translateModKey(content.shortcut)}</kbd>`;
}
}
let title;
if (content.title) {
title = I18n.t(content.title);
if (content.shortcut) {
title += ` (${translateModKey(
PLATFORM_KEY_MODIFIER
)}+${translateModKey(content.shortcut)})`;
}
}
let name = content.name;
if (!name && content.label) {
name = I18n.t(content.label);
}
return {
icon: content.icon,
name: I18n.t(content.label),
label,
title,
name,
id: { name: content.name, action: content.action },
};
}

View File

@ -7,18 +7,34 @@
.select-kit-body {
box-shadow: none;
width: 230px;
border-radius: 0;
}
.select-kit-row {
padding: 0.75em 0.5em;
padding: 0.65em 0.5em;
border-bottom: 1px solid rgba(var(--primary-low-rgb), 0.5);
display: flex;
align-items: center;
// popup-menu doesnt use description text atm
// it's just easier to align the icon with text then
.icons {
align-self: center;
}
.shortcut {
margin-left: 1em;
}
&:last-child {
border: none;
}
.texts .name {
display: flex;
align-items: center;
}
.texts .name,
.icons .d-icon {
font-size: var(--font-0);

View File

@ -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.37.1] - 2024-08-21
- Added support for `shortcut` in `addComposerToolbarPopupMenuOption` which allows to add a keyboard shortcut to the popup menu option.
## [1.37.0] - 2024-08-19
- Added `addAboutPageActivity` which allows plugins/TCs to register a custom site activity item in the new /about page. Requires the server-side `register_stat` plugin API.

View File

@ -13,7 +13,6 @@ import {
query,
} from "discourse/tests/helpers/qunit-helpers";
import selectKit from "discourse/tests/helpers/select-kit-helper";
import I18n from "discourse-i18n";
acceptance("Discourse Presence Plugin", function (needs) {
needs.user({ whisperer: true });
@ -83,7 +82,7 @@ acceptance("Discourse Presence Plugin", function (needs) {
const menu = selectKit(".toolbar-popup-menu-options");
await menu.expand();
await menu.selectRowByName(I18n.t("composer.toggle_whisper"));
await menu.selectRowByName("toggle-whisper");
assert.strictEqual(
count(".composer-actions svg.d-icon-far-eye-slash"),

View File

@ -33,7 +33,7 @@ describe "Table Builder", type: :system do
visit("/latest")
page.find("#create-topic").click
page.find(".toolbar-popup-menu-options").click
page.find(".select-kit-row[data-name='Insert Table']").click
page.find(".select-kit-row[data-name='toggle-spreadsheet']").click
insert_table_modal.type_in_cell(0, 0, "Item 1")
insert_table_modal.type_in_cell(0, 1, "Item 2")
insert_table_modal.type_in_cell(0, 2, "Item 3")
@ -59,7 +59,7 @@ describe "Table Builder", type: :system do
visit("/latest")
page.find("#create-topic").click
page.find(".toolbar-popup-menu-options").click
page.find(".select-kit-row[data-name='Insert Table']").click
page.find(".select-kit-row[data-name='toggle-spreadsheet']").click
insert_table_modal.cancel
expect(page).to have_no_css(".insert-table-modal")
end
@ -68,7 +68,7 @@ describe "Table Builder", type: :system do
visit("/latest")
page.find("#create-topic").click
page.find(".toolbar-popup-menu-options").click
page.find(".select-kit-row[data-name='Insert Table']").click
page.find(".select-kit-row[data-name='toggle-spreadsheet']").click
insert_table_modal.type_in_cell(0, 0, "Item 1")
insert_table_modal.cancel
expect(page).to have_css(".dialog-container .dialog-content")