grammy-inline-menu
Advanced tools
Comparing version 8.0.1 to 9.0.0
@@ -1,3 +0,3 @@ | ||
import type { ConstOrPromise, ContextPathFunc, RegExpLike } from './generic-types.js'; | ||
export type ActionFunc<Context> = (context: Context, path: string) => ConstOrPromise<string | boolean>; | ||
import type { ContextPathFunc, RegExpLike } from './generic-types.js'; | ||
export type ActionFunc<Context> = ContextPathFunc<Context, string | boolean>; | ||
export type ButtonAction<Context> = { | ||
@@ -8,5 +8,5 @@ readonly trigger: RegExpLike; | ||
export declare class ActionHive<Context> { | ||
private readonly _actions; | ||
#private; | ||
add(trigger: RegExpLike, doFunction: ActionFunc<Context>, hide: undefined | ContextPathFunc<Context, boolean>): void; | ||
list(path: RegExpLike): ReadonlySet<ButtonAction<Context>>; | ||
} |
@@ -1,23 +0,13 @@ | ||
"use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
exports.ActionHive = void 0; | ||
const path_js_1 = require("./path.js"); | ||
class ActionHive { | ||
constructor() { | ||
Object.defineProperty(this, "_actions", { | ||
enumerable: true, | ||
configurable: true, | ||
writable: true, | ||
value: new Set() | ||
}); | ||
} | ||
import { combineTrigger, ensureTriggerChild } from './path.js'; | ||
export class ActionHive { | ||
#actions = new Set(); | ||
add(trigger, doFunction, hide) { | ||
(0, path_js_1.ensureTriggerChild)(trigger); | ||
const alreadyExisting = [...this._actions] | ||
ensureTriggerChild(trigger); | ||
const alreadyExisting = [...this.#actions] | ||
.map(o => o.trigger.source) | ||
.includes(trigger.source); | ||
if (alreadyExisting) { | ||
throw new Error(`The action "${trigger.source.slice(0, -1)}" you wanna add was already added. When you hit the button only the first one will be used and not both. This one can not be accessed then. Change the action code to something different.`); | ||
throw new Error(`The unique identifier "${trigger.source.slice(0, -1)}" you wanna add was already added. When you hit the button only the first one will be used and not both. This one can not be accessed then. Change the unique identifier code to something different.`); | ||
} | ||
this._actions.add({ | ||
this.#actions.add({ | ||
trigger, | ||
@@ -34,5 +24,5 @@ async doFunction(context, path) { | ||
const result = new Set(); | ||
for (const { trigger, doFunction } of this._actions) { | ||
for (const { trigger, doFunction } of this.#actions) { | ||
result.add({ | ||
trigger: (0, path_js_1.combineTrigger)(path, trigger), | ||
trigger: combineTrigger(path, trigger), | ||
doFunction, | ||
@@ -44,3 +34,2 @@ }); | ||
} | ||
exports.ActionHive = ActionHive; | ||
//# sourceMappingURL=action-hive.js.map |
@@ -1,6 +0,3 @@ | ||
"use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
exports.getBodyText = exports.isInvoiceBody = exports.isVenueBody = exports.isLocationBody = exports.isMediaBody = exports.isTextBody = exports.MEDIA_TYPES = void 0; | ||
const generic_types_js_1 = require("./generic-types.js"); | ||
exports.MEDIA_TYPES = [ | ||
import { hasTruthyKey, isObject } from './generic-types.js'; | ||
export const MEDIA_TYPES = [ | ||
'animation', | ||
@@ -16,5 +13,5 @@ 'audio', | ||
} | ||
return exports.MEDIA_TYPES.includes(type); | ||
return MEDIA_TYPES.includes(type); | ||
} | ||
function isTextBody(body) { | ||
export function isTextBody(body) { | ||
if (!body) { | ||
@@ -26,3 +23,3 @@ return false; | ||
} | ||
if (!(0, generic_types_js_1.isObject)(body)) { | ||
if (!isObject(body)) { | ||
return false; | ||
@@ -42,7 +39,6 @@ } | ||
} | ||
return (0, generic_types_js_1.hasTruthyKey)(body, 'text'); | ||
return hasTruthyKey(body, 'text'); | ||
} | ||
exports.isTextBody = isTextBody; | ||
function isMediaBody(body) { | ||
if (!(0, generic_types_js_1.isObject)(body)) { | ||
export function isMediaBody(body) { | ||
if (!isObject(body)) { | ||
return false; | ||
@@ -53,14 +49,14 @@ } | ||
} | ||
return (0, generic_types_js_1.hasTruthyKey)(body, 'media'); | ||
return hasTruthyKey(body, 'media'); | ||
} | ||
exports.isMediaBody = isMediaBody; | ||
function isValidLocation(location) { | ||
return typeof location.latitude === 'number' && typeof location.longitude === 'number'; | ||
return typeof location.latitude === 'number' | ||
&& typeof location.longitude === 'number'; | ||
} | ||
function isLocationBody(body) { | ||
if (!(0, generic_types_js_1.hasTruthyKey)(body, 'location')) { | ||
export function isLocationBody(body) { | ||
if (!hasTruthyKey(body, 'location')) { | ||
return false; | ||
} | ||
// Locations can't have text | ||
if ((0, generic_types_js_1.hasTruthyKey)(body, 'text')) { | ||
if (hasTruthyKey(body, 'text')) { | ||
return false; | ||
@@ -71,9 +67,8 @@ } | ||
} | ||
exports.isLocationBody = isLocationBody; | ||
function isVenueBody(body) { | ||
if (!(0, generic_types_js_1.hasTruthyKey)(body, 'venue')) { | ||
export function isVenueBody(body) { | ||
if (!hasTruthyKey(body, 'venue')) { | ||
return false; | ||
} | ||
// Locations can't have text | ||
if ((0, generic_types_js_1.hasTruthyKey)(body, 'text')) { | ||
if (hasTruthyKey(body, 'text')) { | ||
return false; | ||
@@ -87,19 +82,17 @@ } | ||
} | ||
exports.isVenueBody = isVenueBody; | ||
function isInvoiceBody(body) { | ||
if (!(0, generic_types_js_1.hasTruthyKey)(body, 'invoice')) { | ||
export function isInvoiceBody(body) { | ||
if (!hasTruthyKey(body, 'invoice')) { | ||
return false; | ||
} | ||
// Invoices can't have text | ||
if ((0, generic_types_js_1.hasTruthyKey)(body, 'text')) { | ||
if (hasTruthyKey(body, 'text')) { | ||
return false; | ||
} | ||
const { invoice } = body; | ||
return typeof invoice.title === 'string' && typeof invoice.description === 'string'; | ||
return typeof invoice.title === 'string' | ||
&& typeof invoice.description === 'string'; | ||
} | ||
exports.isInvoiceBody = isInvoiceBody; | ||
function getBodyText(body) { | ||
export function getBodyText(body) { | ||
return typeof body === 'string' ? body : body.text; | ||
} | ||
exports.getBodyText = getBodyText; | ||
//# sourceMappingURL=body.js.map |
@@ -1,3 +0,2 @@ | ||
export declare const DEFAULT_BUTTON_COLUMNS = 6; | ||
export declare const DEFAULT_BUTTON_ROWS = 10; | ||
export declare function clamp(value: number, min: number, max: number): number; | ||
export declare function getRowsOfButtons<T>(buttons: readonly T[], columns?: number, maxRows?: number, page?: number): T[][]; | ||
@@ -4,0 +3,0 @@ export declare function getButtonsOfPage<T>(buttons: readonly T[], columns?: number, maxRows?: number, page?: number): T[]; |
@@ -1,20 +0,18 @@ | ||
"use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
exports.maximumButtonsPerPage = exports.getButtonsAsRows = exports.getButtonsOfPage = exports.getRowsOfButtons = exports.DEFAULT_BUTTON_ROWS = exports.DEFAULT_BUTTON_COLUMNS = void 0; | ||
exports.DEFAULT_BUTTON_COLUMNS = 6; | ||
exports.DEFAULT_BUTTON_ROWS = 10; | ||
function getRowsOfButtons(buttons, columns = exports.DEFAULT_BUTTON_COLUMNS, maxRows = exports.DEFAULT_BUTTON_ROWS, page = 1) { | ||
const DEFAULT_BUTTON_COLUMNS = 6; | ||
const DEFAULT_BUTTON_ROWS = 10; | ||
export function clamp(value, min, max) { | ||
return Math.min(Math.max(value, min), max); | ||
} | ||
export function getRowsOfButtons(buttons, columns = DEFAULT_BUTTON_COLUMNS, maxRows = DEFAULT_BUTTON_ROWS, page = 1) { | ||
const relevantButtons = getButtonsOfPage(buttons, columns, maxRows, page); | ||
return getButtonsAsRows(relevantButtons, columns); | ||
} | ||
exports.getRowsOfButtons = getRowsOfButtons; | ||
function getButtonsOfPage(buttons, columns = exports.DEFAULT_BUTTON_COLUMNS, maxRows = exports.DEFAULT_BUTTON_ROWS, page = 1) { | ||
export function getButtonsOfPage(buttons, columns = DEFAULT_BUTTON_COLUMNS, maxRows = DEFAULT_BUTTON_ROWS, page = 1) { | ||
const buttonsPerPage = maximumButtonsPerPage(columns, maxRows); | ||
const totalPages = Math.ceil(buttons.length / buttonsPerPage); | ||
const selectedPage = Math.max(Math.min(page, totalPages), 1); | ||
const selectedPage = clamp(page, 1, totalPages); | ||
const pageOffset = (selectedPage - 1) * buttonsPerPage; | ||
return buttons.slice(pageOffset, pageOffset + buttonsPerPage); | ||
} | ||
exports.getButtonsOfPage = getButtonsOfPage; | ||
function getButtonsAsRows(buttons, columns = exports.DEFAULT_BUTTON_COLUMNS) { | ||
export function getButtonsAsRows(buttons, columns = DEFAULT_BUTTON_COLUMNS) { | ||
const totalRows = Math.ceil(buttons.length / columns); | ||
@@ -28,7 +26,5 @@ const rows = []; | ||
} | ||
exports.getButtonsAsRows = getButtonsAsRows; | ||
function maximumButtonsPerPage(columns = exports.DEFAULT_BUTTON_COLUMNS, maxRows = exports.DEFAULT_BUTTON_ROWS) { | ||
export function maximumButtonsPerPage(columns = DEFAULT_BUTTON_COLUMNS, maxRows = DEFAULT_BUTTON_ROWS) { | ||
return columns * maxRows; | ||
} | ||
exports.maximumButtonsPerPage = maximumButtonsPerPage; | ||
//# sourceMappingURL=align.js.map |
@@ -1,13 +0,28 @@ | ||
import type { ContextPathFunc } from '../generic-types.js'; | ||
import type { ActionFunc } from '../action-hive.js'; | ||
import type { ConstOrContextPathFunc, ContextPathFunc } from '../generic-types.js'; | ||
export interface BasicOptions<Context> { | ||
/** | ||
* Return true when the button(s) should be hidden and not to be called | ||
*/ | ||
/** Return true when the button(s) should be hidden and not to be called */ | ||
readonly hide?: ContextPathFunc<Context, boolean>; | ||
} | ||
export interface SingleButtonOptions<Context> extends BasicOptions<Context> { | ||
/** | ||
* Decide whether the button should be in the last added row or in a new row. Own row per default, last row when true. | ||
*/ | ||
interface JoinLastRowOption { | ||
/** Decide whether the button should be in the last added row or in a new row. Own row per default, last row when true. */ | ||
readonly joinLastRow?: boolean; | ||
} | ||
export interface SingleButtonOptions<Context> extends BasicOptions<Context>, JoinLastRowOption { | ||
/** Label text on the button */ | ||
readonly text: ConstOrContextPathFunc<Context, string>; | ||
} | ||
export type ManualButtonOptions<Context> = BasicOptions<Context> & JoinLastRowOption; | ||
export interface UrlButtonOptions<Context> extends SingleButtonOptions<Context> { | ||
/** Url where this button should be heading */ | ||
readonly url: ConstOrContextPathFunc<Context, string>; | ||
} | ||
export interface SwitchToChatOptions<Context> extends SingleButtonOptions<Context> { | ||
/** Query that is shown next to the bot username. Can be empty ('') */ | ||
readonly query: ConstOrContextPathFunc<Context, string>; | ||
} | ||
export interface InteractionOptions<Context> extends SingleButtonOptions<Context> { | ||
/** Function which is called when the button is pressed */ | ||
readonly do: ActionFunc<Context>; | ||
} | ||
export {}; |
@@ -1,3 +0,2 @@ | ||
"use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
export {}; | ||
//# sourceMappingURL=basic.js.map |
@@ -0,3 +1,3 @@ | ||
import type { ManyChoicesOptions } from '../choices/index.js'; | ||
import type { ConstOrPromise } from '../generic-types.js'; | ||
import type { ManyChoicesOptions } from '../choices/index.js'; | ||
export type ChooseActionFunc<Context> = (context: Context, key: string) => ConstOrPromise<string | boolean>; | ||
@@ -4,0 +4,0 @@ export interface ChooseOptions<Context> extends ManyChoicesOptions<Context> { |
@@ -1,3 +0,2 @@ | ||
"use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
export {}; | ||
//# sourceMappingURL=choose.js.map |
@@ -1,4 +0,2 @@ | ||
"use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
exports.createPaginationChoices = void 0; | ||
import { clamp } from './align.js'; | ||
/** | ||
@@ -10,14 +8,12 @@ * Creates Choices for the paginator | ||
*/ | ||
function createPaginationChoices(totalPages, currentPage) { | ||
// Numbers have to be within | ||
// currentPage in [1..totalPages] | ||
export function createPaginationChoices(totalPages, currentPage) { | ||
const buttons = {}; | ||
const totalPagesFixed = Math.ceil(totalPages); | ||
const currentPageFinite = (typeof currentPage === 'number' && Number.isFinite(currentPage)) ? currentPage : 1; | ||
const currentPageFixed = Math.max(1, Math.min(totalPagesFixed, Math.floor(currentPageFinite))); | ||
const buttons = {}; | ||
if (!Number.isFinite(totalPagesFixed) || !Number.isFinite(currentPageFixed) || totalPagesFixed < 2) { | ||
if (!Number.isFinite(totalPagesFixed) || totalPagesFixed < 2) { | ||
return buttons; | ||
} | ||
const currentPageFixed = (typeof currentPage === 'number' && !Number.isNaN(currentPage)) | ||
? clamp(currentPage, 1, totalPagesFixed) | ||
: 1; | ||
const before = currentPageFixed - 1; | ||
const after = currentPageFixed + 1; | ||
if (currentPageFixed > 1) { | ||
@@ -30,2 +26,3 @@ if (before > 1) { | ||
buttons[currentPageFixed] = String(currentPageFixed); | ||
const after = currentPageFixed + 1; | ||
if (currentPageFixed < totalPagesFixed) { | ||
@@ -39,3 +36,2 @@ buttons[after] = `▶️ ${after}`; | ||
} | ||
exports.createPaginationChoices = createPaginationChoices; | ||
//# sourceMappingURL=pagination.js.map |
@@ -0,4 +1,4 @@ | ||
import { type ManyChoicesOptions } from '../choices/index.js'; | ||
import type { ConstOrPromise } from '../generic-types.js'; | ||
import type { CallbackButtonTemplate } from '../keyboard.js'; | ||
import type { Choices, ManyChoicesOptions } from '../choices/index.js'; | ||
import type { ConstOrContextFunc, ConstOrPromise } from '../generic-types.js'; | ||
export type IsSetFunction<Context> = (context: Context, key: string) => ConstOrPromise<boolean>; | ||
@@ -13,5 +13,3 @@ export type SetFunction<Context> = (context: Context, key: string, newState: boolean) => ConstOrPromise<string | boolean>; | ||
readonly showFalseEmoji?: boolean; | ||
/** | ||
* Function returning the current state of a given choice. | ||
*/ | ||
/** Function returning the current state of a given choice. */ | ||
readonly isSet: IsSetFunction<Context>; | ||
@@ -23,7 +21,5 @@ /** | ||
readonly set: SetFunction<Context>; | ||
/** | ||
* Format the button text which is visible to the user. | ||
*/ | ||
/** Format the button text which is visible to the user. */ | ||
readonly formatState?: FormatStateFunction<Context>; | ||
} | ||
export declare function generateSelectButtons<Context>(actionPrefix: string, choices: ConstOrContextFunc<Context, Choices>, options: SelectOptions<Context>): (context: Context, path: string) => Promise<CallbackButtonTemplate[][]>; | ||
export declare function generateSelectButtons<Context>(uniqueIdentifierPrefix: string, options: SelectOptions<Context>): (context: Context, path: string) => Promise<CallbackButtonTemplate[][]>; |
@@ -1,9 +0,6 @@ | ||
"use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
exports.generateSelectButtons = void 0; | ||
const index_js_1 = require("../choices/index.js"); | ||
const understand_choices_js_1 = require("../choices/understand-choices.js"); | ||
const prefix_js_1 = require("../prefix.js"); | ||
const align_js_1 = require("./align.js"); | ||
function generateSelectButtons(actionPrefix, choices, options) { | ||
import { createChoiceTextFunction, generateChoicesPaginationButtons, } from '../choices/index.js'; | ||
import { ensureCorrectChoiceKeys, getChoiceKeysFromChoices, } from '../choices/understand-choices.js'; | ||
import { prefixEmoji } from '../prefix.js'; | ||
import { getButtonsAsRows, getButtonsOfPage } from './align.js'; | ||
export function generateSelectButtons(uniqueIdentifierPrefix, options) { | ||
return async (context, path) => { | ||
@@ -13,9 +10,14 @@ if (await options.hide?.(context, path)) { | ||
} | ||
const choicesConstant = typeof choices === 'function' ? await choices(context) : choices; | ||
const choiceKeys = (0, understand_choices_js_1.getChoiceKeysFromChoices)(choicesConstant); | ||
(0, understand_choices_js_1.ensureCorrectChoiceKeys)(actionPrefix, path, choiceKeys); | ||
const textFunction = (0, index_js_1.createChoiceTextFunction)(choicesConstant, options.buttonText); | ||
const formatFunction = options.formatState ?? ((_, textResult, state) => (0, prefix_js_1.prefixEmoji)(textResult, state, { hideFalseEmoji: !options.showFalseEmoji })); | ||
const choicesConstant = typeof options.choices === 'function' | ||
? await options.choices(context) | ||
: options.choices; | ||
const choiceKeys = getChoiceKeysFromChoices(choicesConstant); | ||
ensureCorrectChoiceKeys(uniqueIdentifierPrefix, path, choiceKeys); | ||
const textFunction = createChoiceTextFunction(choicesConstant, options.buttonText); | ||
const formatFunction = options.formatState | ||
?? ((_, textResult, state) => prefixEmoji(textResult, state, { | ||
hideFalseEmoji: !options.showFalseEmoji, | ||
})); | ||
const currentPage = await options.getCurrentPage?.(context); | ||
const keysOfPage = (0, align_js_1.getButtonsOfPage)(choiceKeys, options.columns, options.maxRows, currentPage); | ||
const keysOfPage = getButtonsOfPage(choiceKeys, options.columns, options.maxRows, currentPage); | ||
const buttonsOfPage = await Promise.all(keysOfPage | ||
@@ -27,8 +29,8 @@ .map(async (key) => { | ||
const dropinLetter = state ? 'F' : 'T'; | ||
const relativePath = actionPrefix + dropinLetter + ':' + key; | ||
const relativePath = uniqueIdentifierPrefix + dropinLetter + ':' + key; | ||
return { text, relativePath }; | ||
})); | ||
const rows = (0, align_js_1.getButtonsAsRows)(buttonsOfPage, options.columns); | ||
const rows = getButtonsAsRows(buttonsOfPage, options.columns); | ||
if (options.setPage) { | ||
rows.push((0, index_js_1.generateChoicesPaginationButtons)(actionPrefix, choiceKeys.length, currentPage, options)); | ||
rows.push(generateChoicesPaginationButtons(uniqueIdentifierPrefix, choiceKeys.length, currentPage, options)); | ||
} | ||
@@ -38,3 +40,2 @@ return rows; | ||
} | ||
exports.generateSelectButtons = generateSelectButtons; | ||
//# sourceMappingURL=select.js.map |
@@ -1,3 +0,2 @@ | ||
"use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
export {}; | ||
//# sourceMappingURL=submenu.js.map |
@@ -0,19 +1,13 @@ | ||
import type { ConstOrPromise, ContextPathFunc } from '../generic-types.js'; | ||
import type { CallbackButtonTemplate } from '../keyboard.js'; | ||
import type { ConstOrContextPathFunc, ConstOrPromise, ContextPathFunc } from '../generic-types.js'; | ||
import type { SingleButtonOptions } from './basic.js'; | ||
export type FormatStateFunction<Context> = (context: Context, text: string, state: boolean, path: string) => ConstOrPromise<string>; | ||
export interface ToggleOptions<Context> extends SingleButtonOptions<Context> { | ||
/** | ||
* Function returning the current state. | ||
*/ | ||
/** Function returning the current state. */ | ||
readonly isSet: ContextPathFunc<Context, boolean>; | ||
/** | ||
* Function which is called when a user presses the button. | ||
*/ | ||
/** Function which is called when a user presses the button. */ | ||
readonly set: (context: Context, newState: boolean, path: string) => ConstOrPromise<string | boolean>; | ||
/** | ||
* Format the button text which is visible to the user. | ||
*/ | ||
/** Format the button text which is visible to the user. */ | ||
readonly formatState?: FormatStateFunction<Context>; | ||
} | ||
export declare function generateToggleButton<Context>(text: ConstOrContextPathFunc<Context, string>, actionPrefix: string, options: ToggleOptions<Context>): ContextPathFunc<Context, CallbackButtonTemplate | undefined>; | ||
export declare function generateToggleButton<Context>(uniqueIdentifierPrefix: string, options: ToggleOptions<Context>): ContextPathFunc<Context, CallbackButtonTemplate | undefined>; |
@@ -1,7 +0,5 @@ | ||
"use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
exports.generateToggleButton = void 0; | ||
const prefix_js_1 = require("../prefix.js"); | ||
function generateToggleButton(text, actionPrefix, options) { | ||
const formatFunction = options.formatState ?? ((_, text, state) => (0, prefix_js_1.prefixEmoji)(text, state)); | ||
import { prefixEmoji } from '../prefix.js'; | ||
export function generateToggleButton(uniqueIdentifierPrefix, options) { | ||
const formatFunction = options.formatState | ||
?? ((_, text, state) => prefixEmoji(text, state)); | ||
return async (context, path) => { | ||
@@ -11,11 +9,12 @@ if (await options.hide?.(context, path)) { | ||
} | ||
const textResult = typeof text === 'function' ? await text(context, path) : text; | ||
const textResult = typeof options.text === 'function' | ||
? await options.text(context, path) | ||
: options.text; | ||
const state = await options.isSet(context, path); | ||
return { | ||
text: await formatFunction(context, textResult, state, path), | ||
relativePath: actionPrefix + ':' + (state ? 'false' : 'true'), | ||
relativePath: uniqueIdentifierPrefix + ':' + (state ? 'false' : 'true'), | ||
}; | ||
}; | ||
} | ||
exports.generateToggleButton = generateToggleButton; | ||
//# sourceMappingURL=toggle.js.map |
import type { ConstOrContextFunc, ContextPathFunc } from '../generic-types.js'; | ||
import type { Choices } from './types.js'; | ||
export declare function combineHideAndChoices<Context>(actionPrefix: string, choices: ConstOrContextFunc<Context, Choices>, hide: undefined | ContextPathFunc<Context, boolean>): ContextPathFunc<Context, boolean>; | ||
export declare function combineHideAndChoices<Context>(uniqueIdentifierPrefix: string, choices: ConstOrContextFunc<Context, Choices>, hide: undefined | ContextPathFunc<Context, boolean>): ContextPathFunc<Context, boolean>; |
@@ -1,6 +0,6 @@ | ||
"use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
exports.combineHideAndChoices = void 0; | ||
const understand_choices_js_1 = require("./understand-choices.js"); | ||
function combineHideAndChoices(actionPrefix, choices, hide) { | ||
import { getChoiceKeysFromChoices } from './understand-choices.js'; | ||
export function combineHideAndChoices(uniqueIdentifierPrefix, choices, hide) { | ||
if (!choices) { | ||
throw new TypeError('You have to specify `choices`'); | ||
} | ||
return async (context, path) => { | ||
@@ -10,3 +10,4 @@ if (await hide?.(context, path)) { | ||
} | ||
const match = new RegExp('/' + actionPrefix + ':([^/]+)/?$').exec(path); | ||
const match = new RegExp('/' + uniqueIdentifierPrefix + ':([^/]+)/?$') | ||
.exec(path); | ||
const toBeFound = match?.[1]; | ||
@@ -16,4 +17,6 @@ if (!toBeFound) { | ||
} | ||
const choicesConstant = typeof choices === 'function' ? await choices(context) : choices; | ||
const choiceKeys = (0, understand_choices_js_1.getChoiceKeysFromChoices)(choicesConstant); | ||
const choicesConstant = typeof choices === 'function' | ||
? await choices(context) | ||
: choices; | ||
const choiceKeys = getChoiceKeysFromChoices(choicesConstant); | ||
const keyExists = choiceKeys.includes(toBeFound); | ||
@@ -26,3 +29,2 @@ if (!keyExists) { | ||
} | ||
exports.combineHideAndChoices = combineHideAndChoices; | ||
//# sourceMappingURL=actions.js.map |
import type { CallbackButtonTemplate } from '../keyboard.js'; | ||
import type { ConstOrContextFunc } from '../generic-types.js'; | ||
import type { Choices, ChoiceTextFunc, ManyChoicesOptions } from './types.js'; | ||
export declare function generateChoicesButtons<Context>(actionPrefix: string, isSubmenu: boolean, choices: ConstOrContextFunc<Context, Choices>, options: ManyChoicesOptions<Context>): (context: Context, path: string) => Promise<CallbackButtonTemplate[][]>; | ||
export declare function generateChoicesPaginationButtons<Context>(actionPrefix: string, choiceKeys: number, currentPage: number | undefined, options: ManyChoicesOptions<Context>): CallbackButtonTemplate[]; | ||
export declare function generateChoicesButtons<Context>(uniqueIdentifierPrefix: string, isSubmenu: boolean, options: ManyChoicesOptions<Context>): (context: Context, path: string) => Promise<CallbackButtonTemplate[][]>; | ||
export declare function generateChoicesPaginationButtons<Context>(uniqueIdentifierPrefix: string, choiceKeys: number, currentPage: number | undefined, options: ManyChoicesOptions<Context>): CallbackButtonTemplate[]; | ||
export declare function createChoiceTextFunction<Context>(choices: Choices, buttonText: undefined | ChoiceTextFunc<Context>): ChoiceTextFunc<Context>; |
@@ -1,8 +0,5 @@ | ||
"use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
exports.createChoiceTextFunction = exports.generateChoicesPaginationButtons = exports.generateChoicesButtons = void 0; | ||
const pagination_js_1 = require("../buttons/pagination.js"); | ||
const align_js_1 = require("../buttons/align.js"); | ||
const understand_choices_js_1 = require("./understand-choices.js"); | ||
function generateChoicesButtons(actionPrefix, isSubmenu, choices, options) { | ||
import { getButtonsAsRows, getButtonsOfPage, maximumButtonsPerPage, } from '../buttons/align.js'; | ||
import { createPaginationChoices } from '../buttons/pagination.js'; | ||
import { ensureCorrectChoiceKeys, getChoiceKeysFromChoices, getChoiceTextByKey, } from './understand-choices.js'; | ||
export function generateChoicesButtons(uniqueIdentifierPrefix, isSubmenu, options) { | ||
return async (context, path) => { | ||
@@ -12,17 +9,19 @@ if (await options.hide?.(context, path)) { | ||
} | ||
const choicesConstant = typeof choices === 'function' ? await choices(context) : choices; | ||
const choiceKeys = (0, understand_choices_js_1.getChoiceKeysFromChoices)(choicesConstant); | ||
(0, understand_choices_js_1.ensureCorrectChoiceKeys)(actionPrefix, path, choiceKeys); | ||
const choicesConstant = typeof options.choices === 'function' | ||
? await options.choices(context) | ||
: options.choices; | ||
const choiceKeys = getChoiceKeysFromChoices(choicesConstant); | ||
ensureCorrectChoiceKeys(uniqueIdentifierPrefix, path, choiceKeys); | ||
const textFunction = createChoiceTextFunction(choicesConstant, options.buttonText); | ||
const currentPage = await options.getCurrentPage?.(context); | ||
const keysOfPage = (0, align_js_1.getButtonsOfPage)(choiceKeys, options.columns, options.maxRows, currentPage); | ||
const buttonsOfPage = await Promise.all(keysOfPage | ||
.map(async (key) => { | ||
const keysOfPage = getButtonsOfPage(choiceKeys, options.columns, options.maxRows, currentPage); | ||
const buttonsOfPage = await Promise.all(keysOfPage.map(async (key) => { | ||
const text = await textFunction(context, key); | ||
const relativePath = actionPrefix + ':' + key + (isSubmenu ? '/' : ''); | ||
const relativePath = uniqueIdentifierPrefix + ':' + key | ||
+ (isSubmenu ? '/' : ''); | ||
return { text, relativePath }; | ||
})); | ||
const rows = (0, align_js_1.getButtonsAsRows)(buttonsOfPage, options.columns); | ||
const rows = getButtonsAsRows(buttonsOfPage, options.columns); | ||
if (options.setPage) { | ||
rows.push(generateChoicesPaginationButtons(actionPrefix, choiceKeys.length, currentPage, options)); | ||
rows.push(generateChoicesPaginationButtons(uniqueIdentifierPrefix, choiceKeys.length, currentPage, options)); | ||
} | ||
@@ -32,11 +31,10 @@ return rows; | ||
} | ||
exports.generateChoicesButtons = generateChoicesButtons; | ||
function generateChoicesPaginationButtons(actionPrefix, choiceKeys, currentPage, options) { | ||
const entriesPerPage = (0, align_js_1.maximumButtonsPerPage)(options.columns, options.maxRows); | ||
export function generateChoicesPaginationButtons(uniqueIdentifierPrefix, choiceKeys, currentPage, options) { | ||
const entriesPerPage = maximumButtonsPerPage(options.columns, options.maxRows); | ||
const totalPages = choiceKeys / entriesPerPage; | ||
const pageRecord = (0, pagination_js_1.createPaginationChoices)(totalPages, currentPage); | ||
const pageRecord = createPaginationChoices(totalPages, currentPage); | ||
const pageKeys = Object.keys(pageRecord).map(Number); | ||
const pageButtons = pageKeys | ||
.map((page) => ({ | ||
relativePath: `${actionPrefix}P:${page}`, | ||
relativePath: `${uniqueIdentifierPrefix}P:${page}`, | ||
text: pageRecord[page], | ||
@@ -46,10 +44,8 @@ })); | ||
} | ||
exports.generateChoicesPaginationButtons = generateChoicesPaginationButtons; | ||
function createChoiceTextFunction(choices, buttonText) { | ||
export function createChoiceTextFunction(choices, buttonText) { | ||
if (buttonText) { | ||
return buttonText; | ||
} | ||
return (_, key) => (0, understand_choices_js_1.getChoiceTextByKey)(choices, key); | ||
return (_, key) => getChoiceTextByKey(choices, key); | ||
} | ||
exports.createChoiceTextFunction = createChoiceTextFunction; | ||
//# sourceMappingURL=buttons.js.map |
@@ -1,20 +0,4 @@ | ||
"use strict"; | ||
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { | ||
if (k2 === undefined) k2 = k; | ||
var desc = Object.getOwnPropertyDescriptor(m, k); | ||
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { | ||
desc = { enumerable: true, get: function() { return m[k]; } }; | ||
} | ||
Object.defineProperty(o, k2, desc); | ||
}) : (function(o, m, k, k2) { | ||
if (k2 === undefined) k2 = k; | ||
o[k2] = m[k]; | ||
})); | ||
var __exportStar = (this && this.__exportStar) || function(m, exports) { | ||
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p); | ||
}; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
__exportStar(require("./actions.js"), exports); | ||
__exportStar(require("./buttons.js"), exports); | ||
__exportStar(require("./types.js"), exports); | ||
export * from './actions.js'; | ||
export * from './buttons.js'; | ||
export * from './types.js'; | ||
//# sourceMappingURL=index.js.map |
import type { BasicOptions } from '../buttons/basic.js'; | ||
import type { ConstOrPromise } from '../generic-types.js'; | ||
import type { GenericPaginationOptions } from '../buttons/pagination.js'; | ||
import type { ConstOrContextFunc, ConstOrPromise } from '../generic-types.js'; | ||
export type Choice = string | number; | ||
@@ -33,6 +33,6 @@ export type ChoiceText = string; | ||
readonly disableChoiceExistsCheck?: boolean; | ||
/** | ||
* Function which has to return the text the user will see on the button of a given choice | ||
*/ | ||
/** Choices the user can pick from */ | ||
readonly choices: ConstOrContextFunc<Context, Choices>; | ||
/** Function which has to return the text the user will see on the button of a given choice */ | ||
readonly buttonText?: ChoiceTextFunc<Context>; | ||
} |
@@ -1,3 +0,2 @@ | ||
"use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
export {}; | ||
//# sourceMappingURL=types.js.map |
@@ -1,6 +0,4 @@ | ||
import type { Choices, ChoicesArray, ChoicesMap } from './types.js'; | ||
export declare function choicesIsArray(choices: Choices): choices is ChoicesArray; | ||
export declare function choicesIsMap(choices: Choices): choices is ChoicesMap; | ||
import type { Choices } from './types.js'; | ||
export declare function getChoiceKeysFromChoices(choices: Choices): string[]; | ||
export declare function getChoiceTextByKey(choices: Choices, key: string): string; | ||
export declare function ensureCorrectChoiceKeys(actionPrefix: string, path: string, choiceKeys: readonly string[]): void; | ||
export declare function ensureCorrectChoiceKeys(uniqueIdentifierPrefix: string, path: string, choiceKeys: readonly string[]): void; |
@@ -1,13 +0,8 @@ | ||
"use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
exports.ensureCorrectChoiceKeys = exports.getChoiceTextByKey = exports.getChoiceKeysFromChoices = exports.choicesIsMap = exports.choicesIsArray = void 0; | ||
function choicesIsArray(choices) { | ||
return Array.isArray(choices); | ||
} | ||
exports.choicesIsArray = choicesIsArray; | ||
function choicesIsMap(choices) { | ||
return choices instanceof Map; | ||
} | ||
exports.choicesIsMap = choicesIsMap; | ||
function getChoiceKeysFromChoices(choices) { | ||
export function getChoiceKeysFromChoices(choices) { | ||
if (choicesIsArray(choices)) { | ||
@@ -21,4 +16,3 @@ return choices.map(String); | ||
} | ||
exports.getChoiceKeysFromChoices = getChoiceKeysFromChoices; | ||
function getChoiceTextByKey(choices, key) { | ||
export function getChoiceTextByKey(choices, key) { | ||
if (choicesIsArray(choices)) { | ||
@@ -36,10 +30,8 @@ return key; | ||
} | ||
exports.getChoiceTextByKey = getChoiceTextByKey; | ||
function ensureCorrectChoiceKeys(actionPrefix, path, choiceKeys) { | ||
export function ensureCorrectChoiceKeys(uniqueIdentifierPrefix, path, choiceKeys) { | ||
const containSlashExample = choiceKeys.find(o => o.includes('/')); | ||
if (containSlashExample) { | ||
throw new Error(`Choices can not contain '/'. Found '${containSlashExample}' in action '${actionPrefix}' at path '${path}'.`); | ||
throw new Error(`Choices can not contain '/'. Found '${containSlashExample}' in unique identifier '${uniqueIdentifierPrefix}' at path '${path}'.`); | ||
} | ||
} | ||
exports.ensureCorrectChoiceKeys = ensureCorrectChoiceKeys; | ||
//# sourceMappingURL=understand-choices.js.map |
@@ -1,13 +0,8 @@ | ||
"use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
exports.filterNonNullable = exports.isRegExpExecArray = exports.hasTruthyKey = exports.isObject = void 0; | ||
function isObject(something) { | ||
export function isObject(something) { | ||
return typeof something === 'object' && something !== null; | ||
} | ||
exports.isObject = isObject; | ||
function hasTruthyKey(something, key) { | ||
export function hasTruthyKey(something, key) { | ||
return isObject(something) && key in something && Boolean(something[key]); | ||
} | ||
exports.hasTruthyKey = hasTruthyKey; | ||
function isRegExpExecArray(something) { | ||
export function isRegExpExecArray(something) { | ||
if (!Array.isArray(something)) { | ||
@@ -24,8 +19,6 @@ return false; | ||
} | ||
exports.isRegExpExecArray = isRegExpExecArray; | ||
// TODO: remove when .filter(o => o !== undefined) works. | ||
function filterNonNullable() { | ||
export function filterNonNullable() { | ||
return (o) => o !== null && o !== undefined; | ||
} | ||
exports.filterNonNullable = filterNonNullable; | ||
//# sourceMappingURL=generic-types.js.map |
@@ -1,6 +0,6 @@ | ||
export { getMenuOfPath } from './path.js'; | ||
export type { Body } from './body.js'; | ||
export * from './menu-middleware.js'; | ||
export * from './menu-template.js'; | ||
export { getMenuOfPath } from './path.js'; | ||
export * from './row-creators/index.js'; | ||
export * from './send-menu.js'; | ||
export type { Body } from './body.js'; |
@@ -1,24 +0,6 @@ | ||
"use strict"; | ||
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { | ||
if (k2 === undefined) k2 = k; | ||
var desc = Object.getOwnPropertyDescriptor(m, k); | ||
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { | ||
desc = { enumerable: true, get: function() { return m[k]; } }; | ||
} | ||
Object.defineProperty(o, k2, desc); | ||
}) : (function(o, m, k, k2) { | ||
if (k2 === undefined) k2 = k; | ||
o[k2] = m[k]; | ||
})); | ||
var __exportStar = (this && this.__exportStar) || function(m, exports) { | ||
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p); | ||
}; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
exports.getMenuOfPath = void 0; | ||
var path_js_1 = require("./path.js"); | ||
Object.defineProperty(exports, "getMenuOfPath", { enumerable: true, get: function () { return path_js_1.getMenuOfPath; } }); | ||
__exportStar(require("./menu-middleware.js"), exports); | ||
__exportStar(require("./menu-template.js"), exports); | ||
__exportStar(require("./row-creators/index.js"), exports); | ||
__exportStar(require("./send-menu.js"), exports); | ||
export * from './menu-middleware.js'; | ||
export * from './menu-template.js'; | ||
export { getMenuOfPath } from './path.js'; | ||
export * from './row-creators/index.js'; | ||
export * from './send-menu.js'; | ||
//# sourceMappingURL=index.js.map |
import type { InlineKeyboardButton as TelegramInlineKeyboardButton } from 'grammy/types'; | ||
import type { ReadonlyDeep } from 'type-fest'; | ||
import type { ConstOrContextPathFunc, ContextPathFunc } from './generic-types.js'; | ||
import { type ConstOrContextPathFunc, type ContextPathFunc } from './generic-types.js'; | ||
export type CallbackButtonTemplate = { | ||
@@ -15,3 +15,3 @@ readonly text: string; | ||
export declare class Keyboard<Context> { | ||
private readonly _entries; | ||
#private; | ||
addCreator(creator: ButtonTemplateRowGenerator<Context>): void; | ||
@@ -18,0 +18,0 @@ add(joinLastRow: boolean, ...buttons: ReadonlyArray<UncreatedTemplate<Context>>): void; |
@@ -1,7 +0,4 @@ | ||
"use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
exports.Keyboard = void 0; | ||
const node_buffer_1 = require("node:buffer"); | ||
const path_js_1 = require("./path.js"); | ||
const generic_types_js_1 = require("./generic-types.js"); | ||
import { Buffer } from 'node:buffer'; | ||
import { filterNonNullable, } from './generic-types.js'; | ||
import { combinePath } from './path.js'; | ||
function isRow(entry) { | ||
@@ -13,16 +10,9 @@ return Array.isArray(entry); | ||
} | ||
class Keyboard { | ||
constructor() { | ||
Object.defineProperty(this, "_entries", { | ||
enumerable: true, | ||
configurable: true, | ||
writable: true, | ||
value: [] | ||
}); | ||
} | ||
export class Keyboard { | ||
#entries = []; | ||
addCreator(creator) { | ||
this._entries.push(creator); | ||
this.#entries.push(creator); | ||
} | ||
add(joinLastRow, ...buttons) { | ||
const lastEntry = this._entries.slice(-1)[0]; | ||
const lastEntry = this.#entries.at(-1); | ||
if (joinLastRow && isRow(lastEntry)) { | ||
@@ -32,3 +22,3 @@ lastEntry.push(...buttons); | ||
} | ||
this._entries.push([...buttons]); | ||
this.#entries.push([...buttons]); | ||
} | ||
@@ -38,3 +28,3 @@ async render(context, path) { | ||
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types | ||
this._entries.map(async (o) => entryToRows(o, context, path))); | ||
this.#entries.map(async (o) => entryToRows(o, context, path))); | ||
const rows = arrayOfRowArrays | ||
@@ -47,3 +37,2 @@ .flat(1) | ||
} | ||
exports.Keyboard = Keyboard; | ||
async function entryToRows(entry, context, path) { | ||
@@ -54,12 +43,13 @@ if (typeof entry === 'function') { | ||
const buttonsInRow = await Promise.all(entry.map(async (button) => typeof button === 'function' ? button(context, path) : button)); | ||
const filtered = buttonsInRow.filter((0, generic_types_js_1.filterNonNullable)()); | ||
const filtered = buttonsInRow.filter(filterNonNullable()); | ||
return [filtered]; | ||
} | ||
function renderRow(templates, path) { | ||
return templates | ||
.map(template => isCallbackButtonTemplate(template) ? renderCallbackButtonTemplate(template, path) : template); | ||
return templates.map(template => isCallbackButtonTemplate(template) | ||
? renderCallbackButtonTemplate(template, path) | ||
: template); | ||
} | ||
function renderCallbackButtonTemplate(template, path) { | ||
const absolutePath = (0, path_js_1.combinePath)(path, template.relativePath); | ||
const absolutePathLength = node_buffer_1.Buffer.byteLength(absolutePath, 'utf8'); | ||
const absolutePath = combinePath(path, template.relativePath); | ||
const absolutePathLength = Buffer.byteLength(absolutePath, 'utf8'); | ||
if (absolutePathLength > 64) { | ||
@@ -66,0 +56,0 @@ throw new Error(`callback_data only supports 1-64 bytes. With this button (${template.relativePath}) it would get too long (${absolutePathLength}). Full path: ${absolutePath}`); |
@@ -0,3 +1,3 @@ | ||
import type { ButtonAction } from './action-hive.js'; | ||
import type { Body } from './body.js'; | ||
import type { ButtonAction } from './action-hive.js'; | ||
import type { ContextPathFunc, RegExpLike } from './generic-types.js'; | ||
@@ -12,5 +12,6 @@ import type { InlineKeyboard } from './keyboard.js'; | ||
export type Submenu<Context> = { | ||
readonly action: RegExpLike; | ||
/** Unique within the current menu depth */ | ||
readonly trigger: RegExpLike; | ||
readonly hide: undefined | ContextPathFunc<Context, boolean>; | ||
readonly menu: MenuLike<Context>; | ||
}; |
@@ -1,3 +0,2 @@ | ||
"use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
export {}; | ||
//# sourceMappingURL=menu-like.js.map |
@@ -1,5 +0,5 @@ | ||
import type { Context as BaseContext } from 'grammy'; | ||
import { type Context as BaseContext } from 'grammy'; | ||
import type { RegExpLike } from './generic-types.js'; | ||
import type { MenuLike } from './menu-like.js'; | ||
import type { SendMenuFunc } from './send-menu.js'; | ||
import { type SendMenuFunc } from './send-menu.js'; | ||
export type Options<Context> = { | ||
@@ -14,7 +14,6 @@ /** | ||
export declare class MenuMiddleware<Context extends BaseContext> { | ||
#private; | ||
readonly rootTrigger: string | RegExpLike; | ||
readonly rootMenu: MenuLike<Context>; | ||
readonly options: Options<Context>; | ||
private readonly _sendMenu; | ||
private readonly _responder; | ||
constructor(rootTrigger: string | RegExpLike, rootMenu: MenuLike<Context>, options?: Options<Context>); | ||
@@ -28,3 +27,3 @@ /** | ||
*/ | ||
replyToContext(context: Context, path?: string | RegExpLike): Promise<import("@grammyjs/types").Message.DocumentMessage | import("@grammyjs/types").Message.AudioMessage | import("@grammyjs/types").Message.PhotoMessage | import("@grammyjs/types").Message.VideoMessage | import("@grammyjs/types").Message.LocationMessage | import("@grammyjs/types").Message.InvoiceMessage | import("@grammyjs/types").Message.TextMessage>; | ||
replyToContext(context: Context, path?: string | RegExpLike): Promise<import("@grammyjs/types").Message.DocumentMessage | import("@grammyjs/types").Message.TextMessage | import("@grammyjs/types").Message.LocationMessage | import("@grammyjs/types").Message.InvoiceMessage | import("@grammyjs/types").Message.PhotoMessage | import("@grammyjs/types").Message.AudioMessage | import("@grammyjs/types").Message.VideoMessage>; | ||
/** | ||
@@ -31,0 +30,0 @@ * The tree structure can be shown for debugging purposes. |
@@ -1,42 +0,17 @@ | ||
"use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
exports.MenuMiddleware = void 0; | ||
const grammy_1 = require("grammy"); | ||
const path_js_1 = require("./path.js"); | ||
const send_menu_js_1 = require("./send-menu.js"); | ||
class MenuMiddleware { | ||
import { Composer } from 'grammy'; | ||
import { combinePath, combineTrigger, createRootMenuTrigger } from './path.js'; | ||
import { editMenuOnContext, replyMenuToContext, } from './send-menu.js'; | ||
export class MenuMiddleware { | ||
rootTrigger; | ||
rootMenu; | ||
options; | ||
#sendMenu; | ||
#responder; | ||
constructor(rootTrigger, rootMenu, options = {}) { | ||
Object.defineProperty(this, "rootTrigger", { | ||
enumerable: true, | ||
configurable: true, | ||
writable: true, | ||
value: rootTrigger | ||
}); | ||
Object.defineProperty(this, "rootMenu", { | ||
enumerable: true, | ||
configurable: true, | ||
writable: true, | ||
value: rootMenu | ||
}); | ||
Object.defineProperty(this, "options", { | ||
enumerable: true, | ||
configurable: true, | ||
writable: true, | ||
value: options | ||
}); | ||
Object.defineProperty(this, "_sendMenu", { | ||
enumerable: true, | ||
configurable: true, | ||
writable: true, | ||
value: void 0 | ||
}); | ||
Object.defineProperty(this, "_responder", { | ||
enumerable: true, | ||
configurable: true, | ||
writable: true, | ||
value: void 0 | ||
}); | ||
const rootTriggerRegex = (0, path_js_1.createRootMenuTrigger)(rootTrigger); | ||
this._responder = createResponder(rootTriggerRegex, () => true, rootMenu); | ||
this._sendMenu = options.sendMenu ?? send_menu_js_1.editMenuOnContext; | ||
this.rootTrigger = rootTrigger; | ||
this.rootMenu = rootMenu; | ||
this.options = options; | ||
const rootTriggerRegex = createRootMenuTrigger(rootTrigger); | ||
this.#responder = createResponder(rootTriggerRegex, () => true, rootMenu); | ||
this.#sendMenu = options.sendMenu ?? editMenuOnContext; | ||
} | ||
@@ -60,7 +35,7 @@ /** | ||
} | ||
const { match, responder } = await getLongestMatchMenuResponder(context, path, this._responder); | ||
const { match, responder } = await getLongestMatchMenuResponder(context, path, this.#responder); | ||
if (!match) { | ||
throw new Error('There is no menu which works with your supplied path: ' + path); | ||
} | ||
return (0, send_menu_js_1.replyMenuToContext)(responder.menu, context, path); | ||
return replyMenuToContext(responder.menu, context, path); | ||
} | ||
@@ -72,16 +47,13 @@ /** | ||
tree() { | ||
return 'Menu Tree\n' + responderTree(this._responder); | ||
return 'Menu Tree\n' + responderTree(this.#responder); | ||
} | ||
middleware() { | ||
const composer = new grammy_1.Composer(); | ||
const trigger = new RegExp(this._responder.trigger.source, this._responder.trigger.flags); | ||
const composer = new Composer(); | ||
const trigger = new RegExp(this.#responder.trigger.source, this.#responder.trigger.flags); | ||
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types | ||
composer.callbackQuery(trigger, async (context, next) => { | ||
if (!('data' in context.callbackQuery)) { | ||
return next(); | ||
} | ||
composer.callbackQuery(trigger, async (context) => { | ||
const path = context.callbackQuery.data; | ||
let target = path; | ||
if (!path.endsWith('/')) { | ||
const { match, responder } = await getLongestMatchActionResponder(context, path, this._responder); | ||
const { match, responder } = await getLongestMatchActionResponder(context, path, this.#responder); | ||
if (match?.[0] && responder.type === 'action') { | ||
@@ -92,6 +64,6 @@ // @ts-expect-error grammy has some more specific type there | ||
if (typeof afterwardsTarget === 'string' && afterwardsTarget) { | ||
target = (0, path_js_1.combinePath)(path, afterwardsTarget); | ||
target = combinePath(path, afterwardsTarget); | ||
} | ||
else if (afterwardsTarget === true) { | ||
target = (0, path_js_1.combinePath)(path, '.'); | ||
target = combinePath(path, '.'); | ||
} | ||
@@ -107,3 +79,3 @@ else if (afterwardsTarget === false) { | ||
if (target) { | ||
const { match, responder } = await getLongestMatchMenuResponder(context, target, this._responder); | ||
const { match, responder } = await getLongestMatchMenuResponder(context, target, this.#responder); | ||
if (!match?.[0]) { | ||
@@ -117,3 +89,3 @@ // TODO: think about using next() in this case? | ||
// @ts-expect-error menu context is not exactly the context type (callback query context vs base context) | ||
await this._sendMenu(responder.menu, context, targetPath); | ||
await this.#sendMenu(responder.menu, context, targetPath); | ||
await context.answerCallbackQuery() | ||
@@ -126,5 +98,5 @@ .catch(catchCallbackOld); | ||
} | ||
exports.MenuMiddleware = MenuMiddleware; | ||
function catchCallbackOld(error) { | ||
if (error instanceof Error && error.message.includes('query is too old and response timeout expired')) { | ||
if (error instanceof Error | ||
&& error.message.includes('query is too old and response timeout expired')) { | ||
// ignore | ||
@@ -136,3 +108,4 @@ return; | ||
function responderMatch(responder, path) { | ||
return new RegExp(responder.trigger.source, responder.trigger.flags).exec(path) ?? undefined; | ||
return new RegExp(responder.trigger.source, responder.trigger.flags) | ||
.exec(path) ?? undefined; | ||
} | ||
@@ -188,3 +161,3 @@ async function getLongestMatchMenuResponder(context, path, current) { | ||
.map((submenu) => { | ||
const submenuTrigger = (0, path_js_1.combineTrigger)(menuTrigger, submenu.action); | ||
const submenuTrigger = combineTrigger(menuTrigger, submenu.trigger); | ||
const canEnterSubmenu = async (context, path) => { | ||
@@ -225,3 +198,3 @@ if (await submenu.hide?.(context, path)) { | ||
text += regexSource | ||
.replace(/\\\//g, '/') | ||
.replaceAll('\\/', '/') | ||
.replace(/^\^/, '') | ||
@@ -228,0 +201,0 @@ .replace(/\$$/, ''); |
@@ -1,24 +0,14 @@ | ||
import type { ActionFunc, ButtonAction } from './action-hive.js'; | ||
import { type ActionFunc, type ButtonAction } from './action-hive.js'; | ||
import type { Body } from './body.js'; | ||
import type { ButtonTemplate, ButtonTemplateRow, InlineKeyboard } from './keyboard.js'; | ||
import type { Choices } from './choices/index.js'; | ||
import type { InteractionOptions, ManualButtonOptions, SingleButtonOptions, SwitchToChatOptions, UrlButtonOptions } from './buttons/basic.js'; | ||
import type { ChooseOptions } from './buttons/choose.js'; | ||
import { type PaginationOptions } from './buttons/pagination.js'; | ||
import { type SelectOptions } from './buttons/select.js'; | ||
import type { ChooseIntoSubmenuOptions, SubmenuOptions } from './buttons/submenu.js'; | ||
import type { ChooseOptions } from './buttons/choose.js'; | ||
import type { ConstOrContextFunc, ConstOrContextPathFunc, ContextPathFunc, RegExpLike } from './generic-types.js'; | ||
import { type ToggleOptions } from './buttons/toggle.js'; | ||
import type { ConstOrContextPathFunc, ContextPathFunc, RegExpLike } from './generic-types.js'; | ||
import { type ButtonTemplate, type ButtonTemplateRow, type InlineKeyboard } from './keyboard.js'; | ||
import type { MenuLike, Submenu } from './menu-like.js'; | ||
import type { PaginationOptions } from './buttons/pagination.js'; | ||
import type { SelectOptions } from './buttons/select.js'; | ||
import type { SingleButtonOptions } from './buttons/basic.js'; | ||
import type { ToggleOptions } from './buttons/toggle.js'; | ||
export interface InteractionOptions<Context> extends SingleButtonOptions<Context> { | ||
/** | ||
* Function which is called when the button is pressed | ||
*/ | ||
readonly do: ActionFunc<Context>; | ||
} | ||
export declare class MenuTemplate<Context> { | ||
private readonly _body; | ||
private readonly _keyboard; | ||
private readonly _actions; | ||
private readonly _submenus; | ||
#private; | ||
constructor(body: ConstOrContextPathFunc<Context, Body>); | ||
@@ -41,5 +31,3 @@ /** | ||
renderActionHandlers(path: RegExpLike): ReadonlySet<ButtonAction<Context>>; | ||
/** | ||
* Lists the submenus used in this menu template. Usage only recommended for advanced usage of this library. | ||
*/ | ||
/** Lists the submenus used in this menu template. Usage only recommended for advanced usage of this library. */ | ||
listSubmenus(): ReadonlySet<Submenu<Context>>; | ||
@@ -49,5 +37,4 @@ /** | ||
* @param button constant or function returning a button representation to be added to the keyboard | ||
* @param options additional options | ||
*/ | ||
manual(button: ConstOrContextPathFunc<Context, ButtonTemplate>, options?: SingleButtonOptions<Context>): void; | ||
manual(button: ConstOrContextPathFunc<Context, ButtonTemplate>, options?: ManualButtonOptions<Context>): void; | ||
/** | ||
@@ -71,23 +58,26 @@ * Allows for manual creation of many buttons. Less user friendly but very customizable. | ||
manualAction(trigger: RegExpLike, action: ActionFunc<Context>): void; | ||
/** | ||
* Add an url button to the keyboard | ||
* @param text text to be displayed on the button | ||
* @param url url where this button should be heading | ||
* @param options additional options | ||
/** Add an url button to the keyboard | ||
* @example | ||
* menuTemplate.url({ | ||
* text: 'Homepage', | ||
* url: 'https://edjopato.de/', | ||
* }); | ||
*/ | ||
url(text: ConstOrContextPathFunc<Context, string>, url: ConstOrContextPathFunc<Context, string>, options?: SingleButtonOptions<Context>): void; | ||
/** | ||
* Add a switch_inline_query button to the keyboard | ||
* @param text text to be displayed on the button | ||
* @param query query that is shown next to the bot username. Can be empty ('') | ||
* @param options additional options | ||
url(options: UrlButtonOptions<Context>): void; | ||
/** Add a switch_inline_query button to the keyboard | ||
* @example | ||
* menuTemplate.switchToChat({ | ||
* text: 'Use the inline mode', | ||
* query: 'prefilled', | ||
* }); | ||
*/ | ||
switchToChat(text: ConstOrContextPathFunc<Context, string>, query: ConstOrContextPathFunc<Context, string>, options?: SingleButtonOptions<Context>): void; | ||
/** | ||
* Add a switch_inline_query_current_chat button to the keyboard | ||
* @param text text to be displayed on the button | ||
* @param query query that is shown next to the bot username. Can be empty ('') | ||
* @param options additional options | ||
switchToChat(options: SwitchToChatOptions<Context>): void; | ||
/** Add a switch_inline_query_current_chat button to the keyboard | ||
* @example | ||
* menuTemplate.switchToCurrentChat({ | ||
* text: 'Try out the inline mode in this chat', | ||
* query: 'prefilled', | ||
* }); | ||
*/ | ||
switchToCurrentChat(text: ConstOrContextPathFunc<Context, string>, query: ConstOrContextPathFunc<Context, string>, options?: SingleButtonOptions<Context>): void; | ||
switchToCurrentChat(options: SwitchToChatOptions<Context>): void; | ||
/** | ||
@@ -97,19 +87,15 @@ * Button which only purpose is to move around the menu on click. | ||
* If you want to execute a function on click use `menuTemplate.interact(…)` instead. | ||
* @param text text to be displayed on the button | ||
* @param relativePath relative target path like 'child/', '..' or '../sibling/ | ||
* @param options additional options | ||
* | ||
* @example menuTemplate.navigate('back to parent menu', '..') | ||
* @example menuTemplate.navigate('to the root menu', '/') | ||
* @example menuTemplate.navigate('to a sibling menu', '../sibling/') | ||
* @example menuTemplate.navigate('..', {text: 'back to parent menu'}) | ||
* @example menuTemplate.navigate('/', {text: 'to the root menu'}) | ||
* @example menuTemplate.navigate('../sibling/', {text: 'to a sibling menu'}) | ||
*/ | ||
navigate(text: ConstOrContextPathFunc<Context, string>, relativePath: string, options?: SingleButtonOptions<Context>): void; | ||
navigate(relativePath: string, options: SingleButtonOptions<Context>): void; | ||
/** | ||
* Add a button to which a function is executed on click. | ||
* You can update the menu afterwards by returning a relative path. If you only want to update the menu or move around use `menuTemplate.navigate(…)` instead. | ||
* @param text text to be displayed on the button | ||
* @param action unique identifier for this button within the menu template | ||
* @param options additional options. Requires `do` as you want to do something when the user pressed the button. | ||
* @param uniqueIdentifier unique identifier for this button within the menu template | ||
* @example | ||
* menuTemplate.interact('Knock Knock', 'unique', { | ||
* menuTemplate.interact('unique', { | ||
* text: 'Knock Knock', | ||
* do: async context => { | ||
@@ -121,3 +107,4 @@ * await context.answerCallbackQuery('Who is there?') | ||
* @example | ||
* menuTemplate.interact('Update the current menu afterwards', 'unique', { | ||
* menuTemplate.interact('unique', { | ||
* text: 'Update the current menu afterwards', | ||
* do: async context => { | ||
@@ -129,12 +116,11 @@ * // do what you want to do | ||
*/ | ||
interact(text: ConstOrContextPathFunc<Context, string>, action: string, options: InteractionOptions<Context>): void; | ||
interact(uniqueIdentifier: string, options: InteractionOptions<Context>): void; | ||
/** | ||
* Add a button to a submenu | ||
* @param text text to be displayed on the button | ||
* @param action unique identifier for this button within the menu template | ||
* @param uniqueIdentifier unique identifier for this button within the menu template | ||
* @param submenu submenu to be entered on click | ||
* @param options additional options | ||
* @example | ||
* const submenuTemplate = new MenuTemplate('I am a submenu') | ||
* submenuTemplate.interact('Text', 'unique', { | ||
* submenuTemplate.interact('unique', { | ||
* text: 'Text', | ||
* do: async ctx => ctx.answerCallbackQuery('You hit a button in a submenu') | ||
@@ -146,19 +132,24 @@ * }) | ||
*/ | ||
submenu(text: ConstOrContextPathFunc<Context, string>, action: string, submenu: MenuLike<Context>, options?: SubmenuOptions<Context>): void; | ||
submenu(uniqueIdentifier: string, submenu: MenuLike<Context>, options: SubmenuOptions<Context>): void; | ||
/** | ||
* Let the user choose one of many options and execute a function for the one the user picked | ||
* @param actionPrefix prefix which is used to create a unique identifier for each of the resulting buttons | ||
* @param choices choices the user can pick from | ||
* @param options additional options. Requires `do` as you want to do something when the user pressed a button. | ||
* @param uniqueIdentifierPrefix prefix which is used to create a unique identifier for each of the resulting buttons | ||
* @example | ||
* menuTemplate.choose('unique', { | ||
* choices: ['walk', 'swim'], | ||
* async do(ctx, key) { | ||
* await ctx.answerCallbackQuery(`Lets ${key}`); | ||
* return '..'; | ||
* } | ||
* }); | ||
*/ | ||
choose(actionPrefix: string, choices: ConstOrContextFunc<Context, Choices>, options: ChooseOptions<Context>): void; | ||
choose(uniqueIdentifierPrefix: string, options: ChooseOptions<Context>): void; | ||
/** | ||
* Submenu which is entered when a user picks one of many choices | ||
* @param actionPrefix prefix which is used to create a unique identifier for each of the resulting buttons | ||
* @param choices choices the user can pick from. Also see `menuTemplate.choose(…)` for examples on choices | ||
* @param uniqueIdentifierPrefix prefix which is used to create a unique identifier for each of the resulting buttons | ||
* @param submenu submenu to be entered when one of the choices is picked | ||
* @param options additional options | ||
* @example | ||
* const submenu = new MenuTemplate<MyContext>(ctx => `Welcome to ${ctx.match[1]}`) | ||
* submenu.interact('Text', 'unique', { | ||
* submenu.interact('unique', { | ||
* text: 'Text', | ||
* do: async ctx => { | ||
@@ -174,11 +165,10 @@ * console.log('Take a look at ctx.match. It contains the chosen city', ctx.match) | ||
*/ | ||
chooseIntoSubmenu(actionPrefix: string, choices: ConstOrContextFunc<Context, Choices>, submenu: MenuLike<Context>, options?: ChooseIntoSubmenuOptions<Context>): void; | ||
chooseIntoSubmenu(uniqueIdentifierPrefix: string, submenu: MenuLike<Context>, options: ChooseIntoSubmenuOptions<Context>): void; | ||
/** | ||
* Let the user select one (or multiple) options from a set of choices | ||
* @param actionPrefix prefix which is used to create a unique identifier for each of the resulting buttons | ||
* @param choices choices the user can pick from. Also see `menuTemplate.choose(…)` for examples on choices | ||
* @param options additional options. Requires `set` and `isSet`. | ||
* @param uniqueIdentifierPrefix prefix which is used to create a unique identifier for each of the resulting buttons | ||
* @example | ||
* // User can select exactly one | ||
* menuTemplate.select('unique', ['at home', 'at work', 'somewhere else'], { | ||
* menuTemplate.select('unique', { | ||
* choices: ['at home', 'at work', 'somewhere else'], | ||
* isSet: (context, key) => context.session.currentLocation === key, | ||
@@ -192,4 +182,5 @@ * set: (context, key) => { | ||
* // User can select one of multiple options | ||
* menuTemplate.select('unique', ['has arms', 'has legs', 'has eyes', 'has wings'], { | ||
* menuTemplate.select('unique', { | ||
* showFalseEmoji: true, | ||
* choices: ['has arms', 'has legs', 'has eyes', 'has wings'], | ||
* isSet: (context, key) => Boolean(context.session.bodyparts[key]), | ||
@@ -202,3 +193,3 @@ * set: (context, key, newState) => { | ||
*/ | ||
select(actionPrefix: string, choices: ConstOrContextFunc<Context, Choices>, options: SelectOptions<Context>): void; | ||
select(uniqueIdentifierPrefix: string, options: SelectOptions<Context>): void; | ||
/** | ||
@@ -208,14 +199,12 @@ * Shows a row of pagination buttons. | ||
* In order to determine which is the current page and how many pages there are `getCurrentPage` and `getTotalPages` are called to which you have to return the current value | ||
* @param actionPrefix prefix which is used to create a unique identifier for each of the resulting buttons | ||
* @param options additional options. Requires `getCurrentPage`, `getTotalPages` and `setPage`. | ||
* @param uniqueIdentifierPrefix prefix which is used to create a unique identifier for each of the resulting buttons | ||
*/ | ||
pagination(actionPrefix: string, options: PaginationOptions<Context>): void; | ||
pagination(uniqueIdentifierPrefix: string, options: PaginationOptions<Context>): void; | ||
/** | ||
* Toogle a value when the button is pressed. | ||
* If you want to toggle multiple values use `menuTemplate.select(…)` | ||
* @param text text to be displayed on the button | ||
* @param actionPrefix unique identifier for this button within the menu template | ||
* @param options additional options. Requires `set` and `isSet`. | ||
* @param uniqueIdentifierPrefix unique identifier for this button within the menu template | ||
* @example | ||
* menuTemplate.toggle('Text', 'unique', { | ||
* menuTemplate.toggle('unique', { | ||
* text: 'Text', | ||
* isSet: context => Boolean(context.session.isFunny), | ||
@@ -229,3 +218,4 @@ * set: (context, newState) => { | ||
* // You can use a custom format for the state instead of the default emoji | ||
* menuTemplate.toggle('Lamp', 'unique', { | ||
* menuTemplate.toggle('unique', { | ||
* text: 'Lamp', | ||
* formatState: (context, text, state) => `${text}: ${state ? 'on' : 'off'}`, | ||
@@ -239,3 +229,3 @@ * isSet: context => Boolean(context.session.lamp), | ||
*/ | ||
toggle(text: ConstOrContextPathFunc<Context, string>, actionPrefix: string, options: ToggleOptions<Context>): void; | ||
toggle(uniqueIdentifierPrefix: string, options: ToggleOptions<Context>): void; | ||
} |
@@ -1,40 +0,17 @@ | ||
"use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
exports.MenuTemplate = void 0; | ||
const action_hive_js_1 = require("./action-hive.js"); | ||
const index_js_1 = require("./choices/index.js"); | ||
const pagination_js_1 = require("./buttons/pagination.js"); | ||
const path_js_1 = require("./path.js"); | ||
const select_js_1 = require("./buttons/select.js"); | ||
const toggle_js_1 = require("./buttons/toggle.js"); | ||
const keyboard_js_1 = require("./keyboard.js"); | ||
class MenuTemplate { | ||
import { ActionHive, } from './action-hive.js'; | ||
import { createPaginationChoices, } from './buttons/pagination.js'; | ||
import { generateSelectButtons } from './buttons/select.js'; | ||
import { generateToggleButton } from './buttons/toggle.js'; | ||
import { combineHideAndChoices, generateChoicesButtons, } from './choices/index.js'; | ||
import { Keyboard, } from './keyboard.js'; | ||
import { ensureTriggerChild } from './path.js'; | ||
export class MenuTemplate { | ||
#body; | ||
#keyboard = new Keyboard(); | ||
#actions = new ActionHive(); | ||
#submenus = new Set(); | ||
constructor( | ||
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types | ||
body) { | ||
Object.defineProperty(this, "_body", { | ||
enumerable: true, | ||
configurable: true, | ||
writable: true, | ||
value: void 0 | ||
}); | ||
Object.defineProperty(this, "_keyboard", { | ||
enumerable: true, | ||
configurable: true, | ||
writable: true, | ||
value: new keyboard_js_1.Keyboard() | ||
}); | ||
Object.defineProperty(this, "_actions", { | ||
enumerable: true, | ||
configurable: true, | ||
writable: true, | ||
value: new action_hive_js_1.ActionHive() | ||
}); | ||
Object.defineProperty(this, "_submenus", { | ||
enumerable: true, | ||
configurable: true, | ||
writable: true, | ||
value: new Set() | ||
}); | ||
this._body = typeof body === 'function' ? body : () => body; | ||
this.#body = typeof body === 'function' ? body : () => body; | ||
} | ||
@@ -46,3 +23,3 @@ /** | ||
async renderBody(context, path) { | ||
return this._body(context, path); | ||
return this.#body(context, path); | ||
} | ||
@@ -55,3 +32,3 @@ /** | ||
async renderKeyboard(context, path) { | ||
return this._keyboard.render(context, path); | ||
return this.#keyboard.render(context, path); | ||
} | ||
@@ -63,9 +40,7 @@ /** | ||
renderActionHandlers(path) { | ||
return this._actions.list(path); | ||
return this.#actions.list(path); | ||
} | ||
/** | ||
* Lists the submenus used in this menu template. Usage only recommended for advanced usage of this library. | ||
*/ | ||
/** Lists the submenus used in this menu template. Usage only recommended for advanced usage of this library. */ | ||
listSubmenus() { | ||
return this._submenus; | ||
return this.#submenus; | ||
} | ||
@@ -75,3 +50,2 @@ /** | ||
* @param button constant or function returning a button representation to be added to the keyboard | ||
* @param options additional options | ||
*/ | ||
@@ -81,3 +55,3 @@ manual(button, options = {}) { | ||
if (hide) { | ||
this._keyboard.add(Boolean(options.joinLastRow), async (context, path) => { | ||
this.#keyboard.add(Boolean(options.joinLastRow), async (context, path) => { | ||
if (await hide(context, path)) { | ||
@@ -90,3 +64,3 @@ return undefined; | ||
else { | ||
this._keyboard.add(Boolean(options.joinLastRow), button); | ||
this.#keyboard.add(Boolean(options.joinLastRow), button); | ||
} | ||
@@ -99,3 +73,3 @@ } | ||
manualRow(creator) { | ||
this._keyboard.addCreator(creator); | ||
this.#keyboard.addCreator(creator); | ||
} | ||
@@ -115,11 +89,13 @@ /** | ||
manualAction(trigger, action) { | ||
this._actions.add(trigger, action, undefined); | ||
this.#actions.add(trigger, action, undefined); | ||
} | ||
/** | ||
* Add an url button to the keyboard | ||
* @param text text to be displayed on the button | ||
* @param url url where this button should be heading | ||
* @param options additional options | ||
/** Add an url button to the keyboard | ||
* @example | ||
* menuTemplate.url({ | ||
* text: 'Homepage', | ||
* url: 'https://edjopato.de/', | ||
* }); | ||
*/ | ||
url(text, url, options = {}) { | ||
url(options) { | ||
const { text, url } = options; | ||
this.manual(async (context, path) => ({ | ||
@@ -130,24 +106,32 @@ text: typeof text === 'function' ? await text(context, path) : text, | ||
} | ||
/** | ||
* Add a switch_inline_query button to the keyboard | ||
* @param text text to be displayed on the button | ||
* @param query query that is shown next to the bot username. Can be empty ('') | ||
* @param options additional options | ||
/** Add a switch_inline_query button to the keyboard | ||
* @example | ||
* menuTemplate.switchToChat({ | ||
* text: 'Use the inline mode', | ||
* query: 'prefilled', | ||
* }); | ||
*/ | ||
switchToChat(text, query, options = {}) { | ||
switchToChat(options) { | ||
const { text, query } = options; | ||
this.manual(async (context, path) => ({ | ||
text: typeof text === 'function' ? await text(context, path) : text, | ||
switch_inline_query: typeof query === 'function' ? await query(context, path) : query, | ||
switch_inline_query: typeof query === 'function' | ||
? await query(context, path) | ||
: query, | ||
}), options); | ||
} | ||
/** | ||
* Add a switch_inline_query_current_chat button to the keyboard | ||
* @param text text to be displayed on the button | ||
* @param query query that is shown next to the bot username. Can be empty ('') | ||
* @param options additional options | ||
/** Add a switch_inline_query_current_chat button to the keyboard | ||
* @example | ||
* menuTemplate.switchToCurrentChat({ | ||
* text: 'Try out the inline mode in this chat', | ||
* query: 'prefilled', | ||
* }); | ||
*/ | ||
switchToCurrentChat(text, query, options = {}) { | ||
switchToCurrentChat(options) { | ||
const { text, query } = options; | ||
this.manual(async (context, path) => ({ | ||
text: typeof text === 'function' ? await text(context, path) : text, | ||
switch_inline_query_current_chat: typeof query === 'function' ? await query(context, path) : query, | ||
switch_inline_query_current_chat: typeof query === 'function' | ||
? await query(context, path) | ||
: query, | ||
}), options); | ||
@@ -161,12 +145,9 @@ } | ||
* If you want to execute a function on click use `menuTemplate.interact(…)` instead. | ||
* @param text text to be displayed on the button | ||
* @param relativePath relative target path like 'child/', '..' or '../sibling/ | ||
* @param options additional options | ||
* | ||
* @example menuTemplate.navigate('back to parent menu', '..') | ||
* @example menuTemplate.navigate('to the root menu', '/') | ||
* @example menuTemplate.navigate('to a sibling menu', '../sibling/') | ||
* @example menuTemplate.navigate('..', {text: 'back to parent menu'}) | ||
* @example menuTemplate.navigate('/', {text: 'to the root menu'}) | ||
* @example menuTemplate.navigate('../sibling/', {text: 'to a sibling menu'}) | ||
*/ | ||
navigate(text, relativePath, options = {}) { | ||
this._keyboard.add(Boolean(options.joinLastRow), generateCallbackButtonTemplate(text, relativePath, options.hide)); | ||
navigate(relativePath, options) { | ||
this.#keyboard.add(Boolean(options.joinLastRow), generateCallbackButtonTemplate(options.text, relativePath, options.hide)); | ||
} | ||
@@ -176,7 +157,6 @@ /** | ||
* You can update the menu afterwards by returning a relative path. If you only want to update the menu or move around use `menuTemplate.navigate(…)` instead. | ||
* @param text text to be displayed on the button | ||
* @param action unique identifier for this button within the menu template | ||
* @param options additional options. Requires `do` as you want to do something when the user pressed the button. | ||
* @param uniqueIdentifier unique identifier for this button within the menu template | ||
* @example | ||
* menuTemplate.interact('Knock Knock', 'unique', { | ||
* menuTemplate.interact('unique', { | ||
* text: 'Knock Knock', | ||
* do: async context => { | ||
@@ -188,3 +168,4 @@ * await context.answerCallbackQuery('Who is there?') | ||
* @example | ||
* menuTemplate.interact('Update the current menu afterwards', 'unique', { | ||
* menuTemplate.interact('unique', { | ||
* text: 'Update the current menu afterwards', | ||
* do: async context => { | ||
@@ -196,21 +177,17 @@ * // do what you want to do | ||
*/ | ||
interact(text, action, options) { | ||
if ('doFunc' in options) { | ||
throw new TypeError('doFunc was renamed to do'); | ||
} | ||
interact(uniqueIdentifier, options) { | ||
if (typeof options.do !== 'function') { | ||
throw new TypeError('You have to specify `do` in order to have an interaction for this button. If you only want to navigate use `menuTemplate.navigate(…)` instead.'); | ||
} | ||
this._actions.add(new RegExp(action + '$'), options.do, options.hide); | ||
this._keyboard.add(Boolean(options.joinLastRow), generateCallbackButtonTemplate(text, action, options.hide)); | ||
this.#actions.add(new RegExp(uniqueIdentifier + '$'), options.do, options.hide); | ||
this.#keyboard.add(Boolean(options.joinLastRow), generateCallbackButtonTemplate(options.text, uniqueIdentifier, options.hide)); | ||
} | ||
/** | ||
* Add a button to a submenu | ||
* @param text text to be displayed on the button | ||
* @param action unique identifier for this button within the menu template | ||
* @param uniqueIdentifier unique identifier for this button within the menu template | ||
* @param submenu submenu to be entered on click | ||
* @param options additional options | ||
* @example | ||
* const submenuTemplate = new MenuTemplate('I am a submenu') | ||
* submenuTemplate.interact('Text', 'unique', { | ||
* submenuTemplate.interact('unique', { | ||
* text: 'Text', | ||
* do: async ctx => ctx.answerCallbackQuery('You hit a button in a submenu') | ||
@@ -222,45 +199,49 @@ * }) | ||
*/ | ||
submenu(text, action, submenu, options = {}) { | ||
(0, path_js_1.ensureTriggerChild)(action); | ||
const actionRegex = new RegExp(action + '/'); | ||
if ([...this._submenus].map(o => o.action.source).includes(actionRegex.source)) { | ||
throw new Error(`There is already a submenu with the action "${action}". Change the action in order to access both menus.`); | ||
submenu(uniqueIdentifier, submenu, options) { | ||
ensureTriggerChild(uniqueIdentifier); | ||
const trigger = new RegExp(uniqueIdentifier + '/'); | ||
if ([...this.#submenus] | ||
.map(o => o.trigger.source) | ||
.includes(trigger.source)) { | ||
throw new Error(`There is already a submenu with the unique identifier "${uniqueIdentifier}". Change the unique identifier in order to access both menus.`); | ||
} | ||
this._submenus.add({ | ||
action: actionRegex, | ||
this.#submenus.add({ | ||
trigger, | ||
hide: options.hide, | ||
menu: submenu, | ||
}); | ||
this._keyboard.add(Boolean(options.joinLastRow), generateCallbackButtonTemplate(text, action + '/', options.hide)); | ||
this.#keyboard.add(Boolean(options.joinLastRow), generateCallbackButtonTemplate(options.text, uniqueIdentifier + '/', options.hide)); | ||
} | ||
/** | ||
* Let the user choose one of many options and execute a function for the one the user picked | ||
* @param actionPrefix prefix which is used to create a unique identifier for each of the resulting buttons | ||
* @param choices choices the user can pick from | ||
* @param options additional options. Requires `do` as you want to do something when the user pressed a button. | ||
* @param uniqueIdentifierPrefix prefix which is used to create a unique identifier for each of the resulting buttons | ||
* @example | ||
* menuTemplate.choose('unique', { | ||
* choices: ['walk', 'swim'], | ||
* async do(ctx, key) { | ||
* await ctx.answerCallbackQuery(`Lets ${key}`); | ||
* return '..'; | ||
* } | ||
* }); | ||
*/ | ||
choose(actionPrefix, choices, options) { | ||
if ('doFunc' in options) { | ||
throw new TypeError('doFunc was renamed to do'); | ||
choose(uniqueIdentifierPrefix, options) { | ||
if (!options.choices || typeof options.do !== 'function') { | ||
throw new TypeError('You have to specify `choices` and `do` for choose to work.'); | ||
} | ||
if (typeof options.do !== 'function') { | ||
throw new TypeError('You have to specify `do` in order to have an interaction for the buttons.'); | ||
} | ||
const trigger = new RegExp(actionPrefix + ':(.+)$'); | ||
this._actions.add(trigger, async (context, path) => options.do(context, getKeyFromPath(trigger, path)), options.disableChoiceExistsCheck ? options.hide : (0, index_js_1.combineHideAndChoices)(actionPrefix, choices, options.hide)); | ||
const trigger = new RegExp(uniqueIdentifierPrefix + ':(.+)$'); | ||
this.#actions.add(trigger, async (context, path) => options.do(context, getKeyFromPath(trigger, path)), options.disableChoiceExistsCheck ? options.hide : combineHideAndChoices(uniqueIdentifierPrefix, options.choices, options.hide)); | ||
if (options.setPage) { | ||
const pageTrigger = new RegExp(actionPrefix + 'P:(\\d+)$'); | ||
this._actions.add(pageTrigger, setPageAction(pageTrigger, options.setPage), options.hide); | ||
const pageTrigger = new RegExp(uniqueIdentifierPrefix + 'P:(\\d+)$'); | ||
this.#actions.add(pageTrigger, setPageAction(pageTrigger, options.setPage), options.hide); | ||
} | ||
this._keyboard.addCreator((0, index_js_1.generateChoicesButtons)(actionPrefix, false, choices, options)); | ||
this.#keyboard.addCreator(generateChoicesButtons(uniqueIdentifierPrefix, false, options)); | ||
} | ||
/** | ||
* Submenu which is entered when a user picks one of many choices | ||
* @param actionPrefix prefix which is used to create a unique identifier for each of the resulting buttons | ||
* @param choices choices the user can pick from. Also see `menuTemplate.choose(…)` for examples on choices | ||
* @param uniqueIdentifierPrefix prefix which is used to create a unique identifier for each of the resulting buttons | ||
* @param submenu submenu to be entered when one of the choices is picked | ||
* @param options additional options | ||
* @example | ||
* const submenu = new MenuTemplate<MyContext>(ctx => `Welcome to ${ctx.match[1]}`) | ||
* submenu.interact('Text', 'unique', { | ||
* submenu.interact('unique', { | ||
* text: 'Text', | ||
* do: async ctx => { | ||
@@ -276,27 +257,30 @@ * console.log('Take a look at ctx.match. It contains the chosen city', ctx.match) | ||
*/ | ||
chooseIntoSubmenu(actionPrefix, choices, submenu, options = {}) { | ||
(0, path_js_1.ensureTriggerChild)(actionPrefix); | ||
const actionRegex = new RegExp(actionPrefix + ':([^/]+)/'); | ||
if ([...this._submenus].map(o => o.action.source).includes(actionRegex.source)) { | ||
throw new Error(`There is already a submenu with the action "${actionPrefix}". Change the action in order to access both menus.`); | ||
chooseIntoSubmenu(uniqueIdentifierPrefix, submenu, options) { | ||
ensureTriggerChild(uniqueIdentifierPrefix); | ||
const trigger = new RegExp(uniqueIdentifierPrefix + ':([^/]+)/'); | ||
if ([...this.#submenus] | ||
.map(o => o.trigger.source) | ||
.includes(trigger.source)) { | ||
throw new Error(`There is already a submenu with the unique identifier "${uniqueIdentifierPrefix}". Change the unique identifier in order to access both menus.`); | ||
} | ||
this._submenus.add({ | ||
action: actionRegex, | ||
hide: options.disableChoiceExistsCheck ? options.hide : (0, index_js_1.combineHideAndChoices)(actionPrefix, choices, options.hide), | ||
this.#submenus.add({ | ||
trigger, | ||
hide: options.disableChoiceExistsCheck | ||
? options.hide | ||
: combineHideAndChoices(uniqueIdentifierPrefix, options.choices, options.hide), | ||
menu: submenu, | ||
}); | ||
if (options.setPage) { | ||
const pageTrigger = new RegExp(actionPrefix + 'P:(\\d+)$'); | ||
this._actions.add(pageTrigger, setPageAction(pageTrigger, options.setPage), options.hide); | ||
const pageTrigger = new RegExp(uniqueIdentifierPrefix + 'P:(\\d+)$'); | ||
this.#actions.add(pageTrigger, setPageAction(pageTrigger, options.setPage), options.hide); | ||
} | ||
this._keyboard.addCreator((0, index_js_1.generateChoicesButtons)(actionPrefix, true, choices, options)); | ||
this.#keyboard.addCreator(generateChoicesButtons(uniqueIdentifierPrefix, true, options)); | ||
} | ||
/** | ||
* Let the user select one (or multiple) options from a set of choices | ||
* @param actionPrefix prefix which is used to create a unique identifier for each of the resulting buttons | ||
* @param choices choices the user can pick from. Also see `menuTemplate.choose(…)` for examples on choices | ||
* @param options additional options. Requires `set` and `isSet`. | ||
* @param uniqueIdentifierPrefix prefix which is used to create a unique identifier for each of the resulting buttons | ||
* @example | ||
* // User can select exactly one | ||
* menuTemplate.select('unique', ['at home', 'at work', 'somewhere else'], { | ||
* menuTemplate.select('unique', { | ||
* choices: ['at home', 'at work', 'somewhere else'], | ||
* isSet: (context, key) => context.session.currentLocation === key, | ||
@@ -310,4 +294,5 @@ * set: (context, key) => { | ||
* // User can select one of multiple options | ||
* menuTemplate.select('unique', ['has arms', 'has legs', 'has eyes', 'has wings'], { | ||
* menuTemplate.select('unique', { | ||
* showFalseEmoji: true, | ||
* choices: ['has arms', 'has legs', 'has eyes', 'has wings'], | ||
* isSet: (context, key) => Boolean(context.session.bodyparts[key]), | ||
@@ -320,24 +305,23 @@ * set: (context, key, newState) => { | ||
*/ | ||
select(actionPrefix, choices, options) { | ||
if ('setFunc' in options || 'isSetFunc' in options) { | ||
throw new TypeError('setFunc and isSetFunc were renamed to set and isSet'); | ||
select(uniqueIdentifierPrefix, options) { | ||
if (!options.choices | ||
|| typeof options.set !== 'function' | ||
|| typeof options.isSet !== 'function') { | ||
throw new TypeError('You have to specify `choices`, `set` and `isSet` in order to work with select. If you just want to let the user choose between multiple options use `menuTemplate.choose(…)` instead.'); | ||
} | ||
if (typeof options.set !== 'function' || typeof options.isSet !== 'function') { | ||
throw new TypeError('You have to specify `set` and `isSet` in order to work with select. If you just want to let the user choose between multiple options use `menuTemplate.choose(…)` instead.'); | ||
} | ||
const trueTrigger = new RegExp(actionPrefix + 'T:(.+)$'); | ||
this._actions.add(trueTrigger, async (context, path) => { | ||
const trueTrigger = new RegExp(uniqueIdentifierPrefix + 'T:(.+)$'); | ||
this.#actions.add(trueTrigger, async (context, path) => { | ||
const key = getKeyFromPath(trueTrigger, path); | ||
return options.set(context, key, true); | ||
}, options.disableChoiceExistsCheck ? options.hide : (0, index_js_1.combineHideAndChoices)(actionPrefix + 'T', choices, options.hide)); | ||
const falseTrigger = new RegExp(actionPrefix + 'F:(.+)$'); | ||
this._actions.add(falseTrigger, async (context, path) => { | ||
}, options.disableChoiceExistsCheck ? options.hide : combineHideAndChoices(uniqueIdentifierPrefix + 'T', options.choices, options.hide)); | ||
const falseTrigger = new RegExp(uniqueIdentifierPrefix + 'F:(.+)$'); | ||
this.#actions.add(falseTrigger, async (context, path) => { | ||
const key = getKeyFromPath(falseTrigger, path); | ||
return options.set(context, key, false); | ||
}, options.disableChoiceExistsCheck ? options.hide : (0, index_js_1.combineHideAndChoices)(actionPrefix + 'F', choices, options.hide)); | ||
}, options.disableChoiceExistsCheck ? options.hide : combineHideAndChoices(uniqueIdentifierPrefix + 'F', options.choices, options.hide)); | ||
if (options.setPage) { | ||
const pageTrigger = new RegExp(actionPrefix + 'P:(\\d+)$'); | ||
this._actions.add(pageTrigger, setPageAction(pageTrigger, options.setPage), options.hide); | ||
const pageTrigger = new RegExp(uniqueIdentifierPrefix + 'P:(\\d+)$'); | ||
this.#actions.add(pageTrigger, setPageAction(pageTrigger, options.setPage), options.hide); | ||
} | ||
this._keyboard.addCreator((0, select_js_1.generateSelectButtons)(actionPrefix, choices, options)); | ||
this.#keyboard.addCreator(generateSelectButtons(uniqueIdentifierPrefix, options)); | ||
} | ||
@@ -348,7 +332,8 @@ /** | ||
* In order to determine which is the current page and how many pages there are `getCurrentPage` and `getTotalPages` are called to which you have to return the current value | ||
* @param actionPrefix prefix which is used to create a unique identifier for each of the resulting buttons | ||
* @param options additional options. Requires `getCurrentPage`, `getTotalPages` and `setPage`. | ||
* @param uniqueIdentifierPrefix prefix which is used to create a unique identifier for each of the resulting buttons | ||
*/ | ||
pagination(actionPrefix, options) { | ||
if (typeof options.getCurrentPage !== 'function' || typeof options.getTotalPages !== 'function' || typeof options.setPage !== 'function') { | ||
pagination(uniqueIdentifierPrefix, options) { | ||
if (typeof options.getCurrentPage !== 'function' | ||
|| typeof options.getTotalPages !== 'function' | ||
|| typeof options.setPage !== 'function') { | ||
throw new TypeError('You have to specify `getCurrentPage`, `getTotalPages` and `setPage`.'); | ||
@@ -359,8 +344,9 @@ } | ||
const currentPage = await options.getCurrentPage(context); | ||
return (0, pagination_js_1.createPaginationChoices)(totalPages, currentPage); | ||
return createPaginationChoices(totalPages, currentPage); | ||
}; | ||
const trigger = new RegExp(actionPrefix + ':(\\d+)$'); | ||
this._actions.add(trigger, setPageAction(trigger, options.setPage), options.hide); | ||
this._keyboard.addCreator((0, index_js_1.generateChoicesButtons)(actionPrefix, false, paginationChoices, { | ||
const trigger = new RegExp(uniqueIdentifierPrefix + ':(\\d+)$'); | ||
this.#actions.add(trigger, setPageAction(trigger, options.setPage), options.hide); | ||
this.#keyboard.addCreator(generateChoicesButtons(uniqueIdentifierPrefix, false, { | ||
columns: 5, | ||
choices: paginationChoices, | ||
hide: options.hide, | ||
@@ -372,7 +358,6 @@ })); | ||
* If you want to toggle multiple values use `menuTemplate.select(…)` | ||
* @param text text to be displayed on the button | ||
* @param actionPrefix unique identifier for this button within the menu template | ||
* @param options additional options. Requires `set` and `isSet`. | ||
* @param uniqueIdentifierPrefix unique identifier for this button within the menu template | ||
* @example | ||
* menuTemplate.toggle('Text', 'unique', { | ||
* menuTemplate.toggle('unique', { | ||
* text: 'Text', | ||
* isSet: context => Boolean(context.session.isFunny), | ||
@@ -386,3 +371,4 @@ * set: (context, newState) => { | ||
* // You can use a custom format for the state instead of the default emoji | ||
* menuTemplate.toggle('Lamp', 'unique', { | ||
* menuTemplate.toggle('unique', { | ||
* text: 'Lamp', | ||
* formatState: (context, text, state) => `${text}: ${state ? 'on' : 'off'}`, | ||
@@ -396,16 +382,17 @@ * isSet: context => Boolean(context.session.lamp), | ||
*/ | ||
toggle(text, actionPrefix, options) { | ||
if ('setFunc' in options || 'isSetFunc' in options) { | ||
throw new TypeError('setFunc and isSetFunc were renamed to set and isSet'); | ||
toggle(uniqueIdentifierPrefix, options) { | ||
if (!options.text | ||
|| typeof options.set !== 'function' | ||
|| typeof options.isSet !== 'function') { | ||
throw new TypeError('You have to specify `text`, `set` and `isSet` in order to work with toggle. If you just want to implement something more generic use `interact`'); | ||
} | ||
if (typeof options.set !== 'function' || typeof options.isSet !== 'function') { | ||
throw new TypeError('You have to specify `set` and `isSet` in order to work with toggle. If you just want to implement something more generic use `interact`'); | ||
} | ||
this._actions.add(new RegExp(actionPrefix + ':true$'), async (context, path) => options.set(context, true, path), options.hide); | ||
this._actions.add(new RegExp(actionPrefix + ':false$'), async (context, path) => options.set(context, false, path), options.hide); | ||
this._keyboard.add(Boolean(options.joinLastRow), (0, toggle_js_1.generateToggleButton)(text, actionPrefix, options)); | ||
this.#actions.add(new RegExp(uniqueIdentifierPrefix + ':true$'), async (context, path) => options.set(context, true, path), options.hide); | ||
this.#actions.add(new RegExp(uniqueIdentifierPrefix + ':false$'), async (context, path) => options.set(context, false, path), options.hide); | ||
this.#keyboard.add(Boolean(options.joinLastRow), generateToggleButton(uniqueIdentifierPrefix, options)); | ||
} | ||
} | ||
exports.MenuTemplate = MenuTemplate; | ||
function generateCallbackButtonTemplate(text, relativePath, hide) { | ||
if (!text) { | ||
throw new TypeError('you have to specify `text` in order to show a button label'); | ||
} | ||
return async (context, path) => { | ||
@@ -412,0 +399,0 @@ if (await hide?.(context, path)) { |
@@ -1,8 +0,5 @@ | ||
"use strict"; | ||
// Trigger are catching paths | ||
// A menu is having a trigger. When a specific path is matching the trigger, the menu is opened. | ||
// Example: The Trigger /\/events\/e-(.+)\/delete/ will get triggered when the path is called: /events/e-42/delete | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
exports.getMenuOfPath = exports.combinePath = exports.ensurePathMenu = exports.createRootMenuTrigger = exports.combineTrigger = exports.ensureTriggerLastChild = exports.ensureTriggerChild = void 0; | ||
function ensureTriggerChild(trigger) { | ||
export function ensureTriggerChild(trigger) { | ||
const source = typeof trigger === 'string' ? trigger : trigger.source; | ||
@@ -13,4 +10,3 @@ if (source.includes('/') || source.startsWith('..')) { | ||
} | ||
exports.ensureTriggerChild = ensureTriggerChild; | ||
function ensureTriggerLastChild(trigger) { | ||
export function ensureTriggerLastChild(trigger) { | ||
ensureTriggerChild(trigger); | ||
@@ -22,4 +18,3 @@ const source = typeof trigger === 'string' ? trigger : trigger.source; | ||
} | ||
exports.ensureTriggerLastChild = ensureTriggerLastChild; | ||
function combineTrigger(parent, child) { | ||
export function combineTrigger(parent, child) { | ||
if (!parent.source.startsWith('^')) { | ||
@@ -36,4 +31,3 @@ throw new Error('the path has to begin from start in order to prevent mistakes: /^something…\\//'); | ||
} | ||
exports.combineTrigger = combineTrigger; | ||
function createRootMenuTrigger(rootTrigger) { | ||
export function createRootMenuTrigger(rootTrigger) { | ||
if (typeof rootTrigger === 'string') { | ||
@@ -45,3 +39,5 @@ const count = rootTrigger.match(/\//g)?.length; | ||
} | ||
const result = typeof rootTrigger === 'string' ? new RegExp('^' + rootTrigger) : rootTrigger; | ||
const result = typeof rootTrigger === 'string' | ||
? new RegExp('^' + rootTrigger) | ||
: rootTrigger; | ||
if (!result.source.endsWith('/')) { | ||
@@ -61,4 +57,3 @@ throw new Error('the root menu trigger always has to end with a slash: /'); | ||
} | ||
exports.createRootMenuTrigger = createRootMenuTrigger; | ||
function ensurePathMenu(path) { | ||
export function ensurePathMenu(path) { | ||
if (path === '') { | ||
@@ -71,4 +66,3 @@ throw new Error('an empty string is not a path'); | ||
} | ||
exports.ensurePathMenu = ensurePathMenu; | ||
function combinePath(parent, relativePath) { | ||
export function combinePath(parent, relativePath) { | ||
if (relativePath === '') { | ||
@@ -101,4 +95,3 @@ throw new Error('an empty string is not a relative path'); | ||
} | ||
exports.combinePath = combinePath; | ||
function getMenuOfPath(path) { | ||
export function getMenuOfPath(path) { | ||
if (!path.includes('/')) { | ||
@@ -109,3 +102,2 @@ throw new Error('This does not seem like a path. Paths contain slashes to separate the submenus.'); | ||
} | ||
exports.getMenuOfPath = getMenuOfPath; | ||
//# sourceMappingURL=path.js.map |
@@ -14,9 +14,5 @@ export type PrefixOptions = { | ||
readonly prefixFalse?: string; | ||
/** | ||
* Do not show the prefix when true. | ||
*/ | ||
/** Do not show the prefix when true. */ | ||
readonly hideTrueEmoji?: boolean; | ||
/** | ||
* Do not show the prefix when false. | ||
*/ | ||
/** Do not show the prefix when false. */ | ||
readonly hideFalseEmoji?: boolean; | ||
@@ -23,0 +19,0 @@ }; |
@@ -1,6 +0,3 @@ | ||
"use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
exports.prefixText = exports.prefixEmoji = exports.emojiFalse = exports.emojiTrue = void 0; | ||
exports.emojiTrue = '✅'; | ||
exports.emojiFalse = '🚫'; | ||
export const emojiTrue = '✅'; | ||
export const emojiFalse = '🚫'; | ||
/** | ||
@@ -13,7 +10,7 @@ * Prefixes the text with a true / false emoji. | ||
*/ | ||
function prefixEmoji(text, prefix, options = {}) { | ||
export function prefixEmoji(text, prefix, options = {}) { | ||
const internalOptions = { | ||
...options, | ||
prefixTrue: options.prefixTrue ?? exports.emojiTrue, | ||
prefixFalse: options.prefixFalse ?? exports.emojiFalse, | ||
prefixTrue: options.prefixTrue ?? emojiTrue, | ||
prefixFalse: options.prefixFalse ?? emojiFalse, | ||
}; | ||
@@ -23,3 +20,2 @@ const prefixContent = applyOptionsToPrefix(prefix, internalOptions); | ||
} | ||
exports.prefixEmoji = prefixEmoji; | ||
function applyOptionsToPrefix(prefix, options) { | ||
@@ -47,3 +43,3 @@ const { prefixFalse, prefixTrue, hideFalseEmoji, hideTrueEmoji, } = options; | ||
*/ | ||
function prefixText(text, prefix) { | ||
export function prefixText(text, prefix) { | ||
if (!prefix) { | ||
@@ -54,3 +50,2 @@ return text; | ||
} | ||
exports.prefixText = prefixText; | ||
//# sourceMappingURL=prefix.js.map |
@@ -0,3 +1,3 @@ | ||
import type { ConstOrContextPathFunc } from '../generic-types.js'; | ||
import type { CallbackButtonTemplate } from '../keyboard.js'; | ||
import type { ConstOrContextPathFunc } from '../generic-types.js'; | ||
export declare function createBackMainMenuButtons<Context>(backButtonText?: ConstOrContextPathFunc<Context, string>, mainMenuButtonText?: ConstOrContextPathFunc<Context, string>): (context: Context, path: string) => Promise<CallbackButtonTemplate[][]>; |
@@ -1,5 +0,2 @@ | ||
"use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
exports.createBackMainMenuButtons = void 0; | ||
function createBackMainMenuButtons(backButtonText = '🔙back…', mainMenuButtonText = '🔝main menu') { | ||
export function createBackMainMenuButtons(backButtonText = '🔙back…', mainMenuButtonText = '🔝main menu') { | ||
return async (context, path) => { | ||
@@ -11,3 +8,5 @@ const hasMainMenu = mainMenuButtonText && path.startsWith('/'); | ||
row.push({ | ||
text: typeof backButtonText === 'function' ? await backButtonText(context, path) : backButtonText, | ||
text: typeof backButtonText === 'function' | ||
? await backButtonText(context, path) | ||
: backButtonText, | ||
relativePath: '..', | ||
@@ -18,3 +17,5 @@ }); | ||
row.push({ | ||
text: typeof mainMenuButtonText === 'function' ? await mainMenuButtonText(context, path) : mainMenuButtonText, | ||
text: typeof mainMenuButtonText === 'function' | ||
? await mainMenuButtonText(context, path) | ||
: mainMenuButtonText, | ||
relativePath: '/', | ||
@@ -26,3 +27,2 @@ }); | ||
} | ||
exports.createBackMainMenuButtons = createBackMainMenuButtons; | ||
//# sourceMappingURL=back-main-buttons.js.map |
@@ -1,18 +0,2 @@ | ||
"use strict"; | ||
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { | ||
if (k2 === undefined) k2 = k; | ||
var desc = Object.getOwnPropertyDescriptor(m, k); | ||
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { | ||
desc = { enumerable: true, get: function() { return m[k]; } }; | ||
} | ||
Object.defineProperty(o, k2, desc); | ||
}) : (function(o, m, k, k2) { | ||
if (k2 === undefined) k2 = k; | ||
o[k2] = m[k]; | ||
})); | ||
var __exportStar = (this && this.__exportStar) || function(m, exports) { | ||
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p); | ||
}; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
__exportStar(require("./back-main-buttons.js"), exports); | ||
export * from './back-main-buttons.js'; | ||
//# sourceMappingURL=index.js.map |
import type { Api, Context as BaseContext } from 'grammy'; | ||
import type { Message } from 'grammy/types'; | ||
import type { MenuLike } from './menu-like.js'; | ||
/** | ||
* Generic Method which is able to send a menu to a context (given a path where it is) | ||
*/ | ||
/** Generic Method which is able to send a menu to a context (given a path where it is) */ | ||
export type SendMenuFunc<Context> = (menu: MenuLike<Context>, context: Context, path: string) => Promise<unknown>; | ||
@@ -25,3 +23,3 @@ /** | ||
*/ | ||
export declare function replyMenuToContext<Context extends BaseContext>(menu: MenuLike<Context>, context: Context, path: string, other?: Readonly<Record<string, unknown>>): Promise<Message.DocumentMessage | Message.AudioMessage | Message.PhotoMessage | Message.VideoMessage | Message.LocationMessage | Message.InvoiceMessage | Message.TextMessage>; | ||
export declare function replyMenuToContext<Context extends BaseContext>(menu: MenuLike<Context>, context: Context, path: string, other?: Readonly<Record<string, unknown>>): Promise<Message.DocumentMessage | Message.TextMessage | Message.LocationMessage | Message.InvoiceMessage | Message.PhotoMessage | Message.AudioMessage | Message.VideoMessage>; | ||
/** | ||
@@ -34,3 +32,3 @@ * Edit the context into the menu. If thats not possible the current message is deleted and a new message is replied | ||
*/ | ||
export declare function editMenuOnContext<Context extends BaseContext>(menu: MenuLike<Context>, context: Context, path: string, other?: Readonly<Record<string, unknown>>): Promise<boolean | Message.DocumentMessage | Message.AudioMessage | Message.PhotoMessage | Message.VideoMessage | Message.LocationMessage | Message.InvoiceMessage | Message.TextMessage | (import("grammy/types").Update.Edited & Message)>; | ||
export declare function editMenuOnContext<Context extends BaseContext>(menu: MenuLike<Context>, context: Context, path: string, other?: Readonly<Record<string, unknown>>): Promise<boolean | Message.DocumentMessage | Message.TextMessage | Message.LocationMessage | Message.InvoiceMessage | Message.PhotoMessage | Message.AudioMessage | Message.VideoMessage | (import("grammy/types").Update.Edited & Message)>; | ||
/** | ||
@@ -49,3 +47,3 @@ * Delete the message on the context. | ||
*/ | ||
export declare function resendMenuToContext<Context extends BaseContext>(menu: MenuLike<Context>, context: Context, path: string, other?: Readonly<Record<string, unknown>>): Promise<Message.DocumentMessage | Message.AudioMessage | Message.PhotoMessage | Message.VideoMessage | Message.LocationMessage | Message.InvoiceMessage | Message.TextMessage>; | ||
export declare function resendMenuToContext<Context extends BaseContext>(menu: MenuLike<Context>, context: Context, path: string, other?: Readonly<Record<string, unknown>>): Promise<Message.DocumentMessage | Message.TextMessage | Message.LocationMessage | Message.InvoiceMessage | Message.PhotoMessage | Message.AudioMessage | Message.VideoMessage>; | ||
/** | ||
@@ -52,0 +50,0 @@ * Generate a function to send the menu towards a chat from external events |
@@ -1,6 +0,3 @@ | ||
"use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
exports.generateEditMessageIntoMenuFunction = exports.generateSendMenuToChatFunction = exports.resendMenuToContext = exports.deleteMenuFromContext = exports.editMenuOnContext = exports.replyMenuToContext = void 0; | ||
const path_js_1 = require("./path.js"); | ||
const body_js_1 = require("./body.js"); | ||
import { getBodyText, isInvoiceBody, isLocationBody, isMediaBody, isTextBody, isVenueBody, } from './body.js'; | ||
import { ensurePathMenu } from './path.js'; | ||
/** | ||
@@ -13,4 +10,4 @@ * Reply a menu to a context as a new message | ||
*/ | ||
async function replyMenuToContext(menu, context, path, other) { | ||
(0, path_js_1.ensurePathMenu)(path); | ||
export async function replyMenuToContext(menu, context, path, other) { | ||
ensurePathMenu(path); | ||
const body = await menu.renderBody(context, path); | ||
@@ -20,3 +17,2 @@ const keyboard = await menu.renderKeyboard(context, path); | ||
} | ||
exports.replyMenuToContext = replyMenuToContext; | ||
/** | ||
@@ -29,4 +25,4 @@ * Edit the context into the menu. If thats not possible the current message is deleted and a new message is replied | ||
*/ | ||
async function editMenuOnContext(menu, context, path, other = {}) { | ||
(0, path_js_1.ensurePathMenu)(path); | ||
export async function editMenuOnContext(menu, context, path, other = {}) { | ||
ensurePathMenu(path); | ||
const body = await menu.renderBody(context, path); | ||
@@ -38,4 +34,8 @@ const keyboard = await menu.renderKeyboard(context, path); | ||
} | ||
if ((0, body_js_1.isMediaBody)(body)) { | ||
if ('animation' in message || 'audio' in message || 'document' in message || 'photo' in message || 'video' in message) { | ||
if (isMediaBody(body)) { | ||
if ('animation' in message | ||
|| 'audio' in message | ||
|| 'document' in message | ||
|| 'photo' in message | ||
|| 'video' in message) { | ||
return context.editMessageMedia({ | ||
@@ -50,9 +50,8 @@ type: body.type, | ||
} | ||
else if ((0, body_js_1.isLocationBody)(body) || (0, body_js_1.isVenueBody)(body) || (0, body_js_1.isInvoiceBody)(body)) { | ||
else if (isLocationBody(body) || isVenueBody(body) || isInvoiceBody(body)) { | ||
// Dont edit the message, just recreate it. | ||
} | ||
else if ((0, body_js_1.isTextBody)(body)) { | ||
const text = (0, body_js_1.getBodyText)(body); | ||
else if (isTextBody(body)) { | ||
if ('text' in message) { | ||
return context.editMessageText(text, createTextOther(body, keyboard, other)) | ||
return context.editMessageText(getBodyText(body), createTextOther(body, keyboard, other)) | ||
.catch(catchMessageNotModified); | ||
@@ -71,3 +70,2 @@ } | ||
} | ||
exports.editMenuOnContext = editMenuOnContext; | ||
/** | ||
@@ -78,3 +76,3 @@ * Delete the message on the context. | ||
*/ | ||
async function deleteMenuFromContext(context) { | ||
export async function deleteMenuFromContext(context) { | ||
try { | ||
@@ -87,3 +85,2 @@ await context.deleteMessage(); | ||
} | ||
exports.deleteMenuFromContext = deleteMenuFromContext; | ||
/** | ||
@@ -96,3 +93,3 @@ * Deletes to menu of the current context and replies a new one ensuring the menu is at the end of the chat. | ||
*/ | ||
async function resendMenuToContext(menu, context, path, other = {}) { | ||
export async function resendMenuToContext(menu, context, path, other = {}) { | ||
const [menuMessage] = await Promise.all([ | ||
@@ -104,5 +101,5 @@ replyMenuToContext(menu, context, path, other), | ||
} | ||
exports.resendMenuToContext = resendMenuToContext; | ||
function catchMessageNotModified(error) { | ||
if (error instanceof Error && error.message.includes('message is not modified')) { | ||
if (error instanceof Error | ||
&& error.message.includes('message is not modified')) { | ||
// ignore | ||
@@ -116,5 +113,4 @@ return false; | ||
body, keyboard, context, other = {}) { | ||
if ((0, body_js_1.isMediaBody)(body)) { | ||
if (isMediaBody(body)) { | ||
const mediaOther = createSendMediaOther(body, keyboard, other); | ||
// eslint-disable-next-line default-case | ||
switch (body.type) { | ||
@@ -138,15 +134,13 @@ case 'animation': { | ||
} | ||
if ((0, body_js_1.isLocationBody)(body)) { | ||
if (isLocationBody(body)) { | ||
return context.replyWithLocation(body.location.latitude, body.location.longitude, createLocationOther(body, keyboard, other)); | ||
} | ||
if ((0, body_js_1.isVenueBody)(body)) { | ||
const { location, title, address } = body.venue; | ||
return context.replyWithVenue(location.latitude, location.longitude, title, address, createVenueOther(body, keyboard, other)); | ||
if (isVenueBody(body)) { | ||
return context.replyWithVenue(body.venue.location.latitude, body.venue.location.longitude, body.venue.title, body.venue.address, createVenueOther(body, keyboard, other)); | ||
} | ||
if ((0, body_js_1.isInvoiceBody)(body)) { | ||
const { title, description, payload, provider_token, currency, prices } = body.invoice; | ||
return context.replyWithInvoice(title, description, payload, provider_token, currency, prices, createGenericOther(keyboard, other)); | ||
if (isInvoiceBody(body)) { | ||
return context.replyWithInvoice(body.invoice.title, body.invoice.description, body.invoice.payload, body.invoice.provider_token, body.invoice.currency, body.invoice.prices, createGenericOther(keyboard, other)); | ||
} | ||
if ((0, body_js_1.isTextBody)(body)) { | ||
const text = (0, body_js_1.getBodyText)(body); | ||
if (isTextBody(body)) { | ||
const text = getBodyText(body); | ||
return context.reply(text, createTextOther(body, keyboard, other)); | ||
@@ -162,3 +156,3 @@ } | ||
*/ | ||
function generateSendMenuToChatFunction( | ||
export function generateSendMenuToChatFunction( | ||
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types | ||
@@ -169,5 +163,4 @@ telegram, menu, path) { | ||
const keyboard = await menu.renderKeyboard(context, path); | ||
if ((0, body_js_1.isMediaBody)(body)) { | ||
if (isMediaBody(body)) { | ||
const mediaOther = createSendMediaOther(body, keyboard, other); | ||
// eslint-disable-next-line default-case | ||
switch (body.type) { | ||
@@ -191,16 +184,13 @@ case 'animation': { | ||
} | ||
if ((0, body_js_1.isLocationBody)(body)) { | ||
if (isLocationBody(body)) { | ||
return telegram.sendLocation(chatId, body.location.latitude, body.location.longitude, createLocationOther(body, keyboard, other)); | ||
} | ||
if ((0, body_js_1.isVenueBody)(body)) { | ||
const { location, title, address } = body.venue; | ||
return telegram.sendVenue(chatId, location.latitude, location.longitude, title, address, createVenueOther(body, keyboard, other)); | ||
if (isVenueBody(body)) { | ||
return telegram.sendVenue(chatId, body.venue.location.latitude, body.venue.location.longitude, body.venue.title, body.venue.address, createVenueOther(body, keyboard, other)); | ||
} | ||
if ((0, body_js_1.isInvoiceBody)(body)) { | ||
const { title, description, payload, provider_token, currency, prices } = body.invoice; | ||
return telegram.sendInvoice(chatId, title, description, payload, provider_token, currency, prices, createGenericOther(keyboard, other)); | ||
if (isInvoiceBody(body)) { | ||
return telegram.sendInvoice(chatId, body.invoice.title, body.invoice.description, body.invoice.payload, body.invoice.provider_token, body.invoice.currency, body.invoice.prices, createGenericOther(keyboard, other)); | ||
} | ||
if ((0, body_js_1.isTextBody)(body)) { | ||
const text = (0, body_js_1.getBodyText)(body); | ||
return telegram.sendMessage(chatId, text, createTextOther(body, keyboard, other)); | ||
if (isTextBody(body)) { | ||
return telegram.sendMessage(chatId, getBodyText(body), createTextOther(body, keyboard, other)); | ||
} | ||
@@ -210,3 +200,2 @@ throw new Error('The body has to be a string or an object containing text or media. Check the grammy-inline-menu Documentation.'); | ||
} | ||
exports.generateSendMenuToChatFunction = generateSendMenuToChatFunction; | ||
/** | ||
@@ -219,3 +208,3 @@ * Edit the message into the the menu. | ||
*/ | ||
function generateEditMessageIntoMenuFunction( | ||
export function generateEditMessageIntoMenuFunction( | ||
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types | ||
@@ -226,3 +215,3 @@ telegram, menu, path) { | ||
const keyboard = await menu.renderKeyboard(context, path); | ||
if ((0, body_js_1.isMediaBody)(body)) { | ||
if (isMediaBody(body)) { | ||
return telegram.editMessageMedia(chatId, messageId, { | ||
@@ -235,14 +224,13 @@ type: body.type, | ||
} | ||
if ((0, body_js_1.isLocationBody)(body)) { | ||
if (isLocationBody(body)) { | ||
throw new Error('You can not edit into a location body. You have to send the menu as a new message.'); | ||
} | ||
if ((0, body_js_1.isVenueBody)(body)) { | ||
if (isVenueBody(body)) { | ||
throw new Error('You can not edit into a venue body. You have to send the menu as a new message.'); | ||
} | ||
if ((0, body_js_1.isInvoiceBody)(body)) { | ||
if (isInvoiceBody(body)) { | ||
throw new Error('You can not edit into an invoice body. You have to send the menu as a new message.'); | ||
} | ||
if ((0, body_js_1.isTextBody)(body)) { | ||
const text = (0, body_js_1.getBodyText)(body); | ||
return telegram.editMessageText(chatId, messageId, text, createTextOther(body, keyboard, other)); | ||
if (isTextBody(body)) { | ||
return telegram.editMessageText(chatId, messageId, getBodyText(body), createTextOther(body, keyboard, other)); | ||
} | ||
@@ -252,3 +240,2 @@ throw new Error('The body has to be a string or an object containing text or media. Check the grammy-inline-menu Documentation.'); | ||
} | ||
exports.generateEditMessageIntoMenuFunction = generateEditMessageIntoMenuFunction; | ||
function createTextOther(body, keyboard, base) { | ||
@@ -258,3 +245,4 @@ return { | ||
parse_mode: typeof body === 'string' ? undefined : body.parse_mode, | ||
disable_web_page_preview: typeof body !== 'string' && body.disable_web_page_preview, | ||
disable_web_page_preview: typeof body !== 'string' | ||
&& body.disable_web_page_preview, | ||
reply_markup: { | ||
@@ -261,0 +249,0 @@ inline_keyboard: keyboard.map(o => [...o]), |
{ | ||
"name": "grammy-inline-menu", | ||
"version": "8.0.1", | ||
"version": "9.0.0", | ||
"description": "Inline Menus for Telegram made simple. Successor of telegraf-inline-menu.", | ||
@@ -19,3 +19,3 @@ "keywords": [ | ||
"type": "git", | ||
"url": "https://github.com/EdJoPaTo/grammy-inline-menu.git" | ||
"url": "git+https://github.com/EdJoPaTo/grammy-inline-menu.git" | ||
}, | ||
@@ -37,43 +37,31 @@ "bugs": { | ||
"start": "tsc --sourceMap && node --enable-source-maps dist/examples/main-typescript.js", | ||
"test": "tsc --sourceMap && xo && nyc ava" | ||
"test": "tsc --sourceMap && xo && c8 --all node --test --enable-source-maps" | ||
}, | ||
"type": "commonjs", | ||
"type": "module", | ||
"engines": { | ||
"node": ">=14" | ||
"node": ">=18" | ||
}, | ||
"dependencies": { | ||
"type-fest": "^3.0.0" | ||
"type-fest": "^4.0.0" | ||
}, | ||
"peerDependencies": { | ||
"grammy": "^1.10.1" | ||
"grammy": "^1.19.2" | ||
}, | ||
"devDependencies": { | ||
"@sindresorhus/tsconfig": "^3.0.1", | ||
"@types/node": "^14.18.21", | ||
"ava": "^5.0.1", | ||
"@sindresorhus/tsconfig": "^5.0.0", | ||
"@types/node": "^18.17.12", | ||
"c8": "^9.0.0", | ||
"del-cli": "^5.0.0", | ||
"grammy": "^1.10.1", | ||
"nyc": "^15.0.0", | ||
"grammy": "^1.19.2", | ||
"typescript": "^5.0.2", | ||
"xo": "^0.54.0" | ||
"xo": "^0.58.0" | ||
}, | ||
"files": [ | ||
"dist/source", | ||
"!*.test.*" | ||
"!*.test.*", | ||
"!test.*" | ||
], | ||
"main": "./dist/source/index.js", | ||
"types": "./dist/source/index.d.ts", | ||
"nyc": { | ||
"all": true, | ||
"include": [ | ||
"dist/source", | ||
"source" | ||
], | ||
"reporter": [ | ||
"lcov", | ||
"text" | ||
] | ||
}, | ||
"xo": { | ||
"semicolon": false, | ||
"rules": { | ||
@@ -83,3 +71,3 @@ "@typescript-eslint/consistent-type-definitions": "off", | ||
"@typescript-eslint/prefer-readonly-parameter-types": "error", | ||
"ava/no-ignored-test-files": "off" | ||
"unicorn/prevent-abbreviations": "off" | ||
}, | ||
@@ -100,2 +88,3 @@ "overrides": [ | ||
"**/*.test.*", | ||
"**/test.*", | ||
"test/**/*.*" | ||
@@ -110,3 +99,2 @@ ], | ||
"@typescript-eslint/prefer-readonly-parameter-types": "off", | ||
"ava/no-skip-test": "warn", | ||
"max-params": "off", | ||
@@ -113,0 +101,0 @@ "unicorn/prefer-top-level-await": "off" |
439
README.md
@@ -5,9 +5,9 @@ # grammY Inline Menu | ||
![Example shown as a gif](media/example-food.gif) | ||
![Example shown as a GIF](media/example-food.gif) | ||
# Installation | ||
## Installation | ||
```bash | ||
npm install grammy grammy-inline-menu | ||
``` | ||
$ npm install grammy grammy-inline-menu | ||
``` | ||
@@ -18,82 +18,83 @@ Consider using TypeScript with this library as it helps with finding some common mistakes faster. | ||
# Examples | ||
## Examples | ||
## Basic Example | ||
### Basic Example | ||
```ts | ||
import {Bot} from 'grammy' | ||
import {MenuTemplate, MenuMiddleware} from 'grammy-inline-menu' | ||
import { Bot } from 'grammy'; | ||
import { MenuMiddleware, MenuTemplate } from 'grammy-inline-menu'; | ||
const menuTemplate = new MenuTemplate<MyContext>(ctx => `Hey ${ctx.from.first_name}!`) | ||
const menuTemplate = new MenuTemplate<MyContext>((ctx) => | ||
`Hey ${ctx.from.first_name}!` | ||
); | ||
menuTemplate.interact('I am excited!', 'a', { | ||
do: async ctx => { | ||
await ctx.reply('As am I!') | ||
return false | ||
} | ||
}) | ||
menuTemplate.interact('unique', { | ||
text: 'I am excited!', | ||
do: async (ctx) => { | ||
await ctx.reply('As am I!'); | ||
return false; | ||
}, | ||
}); | ||
const bot = new Bot(process.env.BOT_TOKEN) | ||
const bot = new Bot(process.env.BOT_TOKEN); | ||
const menuMiddleware = new MenuMiddleware('/', menuTemplate) | ||
bot.command('start', ctx => menuMiddleware.replyToContext(ctx)) | ||
bot.use(menuMiddleware) | ||
const menuMiddleware = new MenuMiddleware('/', menuTemplate); | ||
bot.command('start', (ctx) => menuMiddleware.replyToContext(ctx)); | ||
bot.use(menuMiddleware); | ||
bot.launch() | ||
await bot.start(); | ||
``` | ||
## More interesting one | ||
### More interesting one | ||
![Example shown as a gif](media/example-food.gif) | ||
![Example shown as a GIF](media/example-food.gif) | ||
Look at the code here: [TypeScript](examples/main-typescript.ts) / [JavaScript (consider using TypeScript)](examples/main-javascript.mjs) | ||
# Migrate from version 4 to 5 | ||
## Version migration | ||
If your project still uses version 4 of this library see [v4 documentation](https://github.com/EdJoPaTo/telegraf-inline-menu/blob/v4.0.1/README.md) and consider refactoring to version 5. | ||
### Migrate from version 6 to 7 | ||
List of things to migrate: | ||
Version 7 switches from Telegraf to [grammY](https://github.com/grammyjs/grammY) as a Telegram Bot framework. | ||
grammY has various benefits over Telegraf as Telegraf is quite old and grammY learned a lot from its mistakes and shortcomings. | ||
- `TelegrafInlineMenu` was split into multiple classes. | ||
When you used `new TelegrafInlineMenu(text)`, you will use `new MenuTemplate(body)` now. | ||
- Applying the menu to the bot via `bot.use` changed. This can now be done with the `MenuMiddleware`. Check the [Basic Example](#Basic-Example) | ||
- `button` and `simpleButton` are combined and renamed into `interact`. See [How can I run a simple method when pressing a button?](#how-can-i-run-a-simple-method-when-pressing-a-button) | ||
- `selectSubmenu` was renamed to `chooseIntoSubmenu` | ||
- `select` was split into `choose` and `select`. See [What's the difference between choose and select?](#Whats-the-difference-between-choose-and-select) | ||
- `question` is moved into a separate library. See [Didn't this menu had a question function?](#Didnt-this-menu-had-a-question-function) | ||
- The menu does not automatically add back and main menu buttons anymore. | ||
Use `menuTemplate.manualRow(createBackMainMenuButtons())` for that at each menu which should include these buttons. | ||
- `setCommand` and `replyMenuMiddleware` were replaced by multiple different functions. See [Can I send the menu manually?](#Can-I-send-the-menu-manually) | ||
```diff | ||
-import {Telegraf} from 'telegraf' | ||
-import {MenuTemplate, MenuMiddleware} from 'telegraf-inline-menu' | ||
+import {Bot} from 'grammy' | ||
+import {MenuTemplate, MenuMiddleware} from 'grammy-inline-menu' | ||
``` | ||
# Migrate from version 5 to 6 | ||
### Migrate from version 8 to 9 | ||
Version 6 switched from Telegraf 3.38 to 4.0. See the [Telegraf migration guide for this set of changes](https://github.com/telegraf/telegraf/releases/tag/v4.0.0). | ||
Version 9 moves `MenuTemplate` arguments into the options object. | ||
This results in shorter lines and easier code readability. | ||
It also allows to inline methods easier. | ||
telegraf-inline-menu is relatively unaffected by this. | ||
The only change required besides the Telegraf changes is the change of `ctx.match`. | ||
Simply add `match` to your `MyContext` type: | ||
```ts | ||
export interface MyContext extends TelegrafContext { | ||
readonly match: RegExpExecArray | undefined; | ||
… | ||
```diff | ||
-menuTemplate.interact((ctx) => ctx.i18n.t('button'), 'unique', { | ||
+menuTemplate.interact('unique', { | ||
+ text: (ctx) => ctx.i18n.t('button'), | ||
do: async (ctx) => { | ||
… | ||
} | ||
} | ||
``` | ||
Telegraf knows when match is available or not. | ||
The default Context does not have match anymore. | ||
telegraf-inline-menu should also know this in a future release. | ||
```diff | ||
-menuTemplate.url('Text', 'https://edjopato.de', { joinLastRow: true }); | ||
+menuTemplate.url({ text: 'Text', url: 'https://edjopato.de', joinLastRow: true }); | ||
``` | ||
# Migrate from version 6 to 7 | ||
Version 7 switches from Telegraf to [grammY](https://github.com/grammyjs/grammY) as a Telegram Bot framework. | ||
grammY has various benefits over Telegraf as Telegraf is quite old and grammY learned a lot from its mistakes and shortcomings. | ||
```diff | ||
-import {Telegraf} from 'telegraf' | ||
-import {MenuTemplate, MenuMiddleware} from 'telegraf-inline-menu' | ||
+import {Bot} from 'grammy' | ||
+import {MenuTemplate, MenuMiddleware} from 'grammy-inline-menu' | ||
-menuTemplate.choose('unique', ['walk', 'swim'], { | ||
+menuTemplate.choose('unique', { | ||
+ choices: ['walk', 'swim'], | ||
do: async (ctx, key) => { | ||
… | ||
} | ||
} | ||
``` | ||
# How does it work | ||
## How does it work | ||
@@ -125,9 +126,9 @@ Telegrams inline keyboards have buttons. | ||
if (ctx.callbackQuery) { | ||
console.log('callback data just happened', ctx.callbackQuery.data) | ||
console.log('callback data just happened', ctx.callbackQuery.data); | ||
} | ||
return next() | ||
}) | ||
return next(); | ||
}); | ||
bot.use(menuMiddleware) | ||
bot.use(menuMiddleware); | ||
``` | ||
@@ -142,3 +143,3 @@ | ||
## Improve the docs | ||
### Improve the docs | ||
@@ -153,5 +154,5 @@ If you have any questions on how the library works head out to the issues and ask ahead. | ||
# FAQ | ||
## FAQ | ||
## Can I use HTML / MarkdownV2 in the message body? | ||
### Can I use HTML / MarkdownV2 in the message body? | ||
@@ -161,12 +162,12 @@ Maybe this is also useful: [NPM package telegram-format](https://github.com/EdJoPaTo/telegram-format) | ||
```ts | ||
const menuTemplate = new MenuTemplate<MyContext>(ctx => { | ||
const text = '_Hey_ *there*!' | ||
return {text, parse_mode: 'Markdown'} | ||
}) | ||
const menuTemplate = new MenuTemplate<MyContext>((ctx) => { | ||
const text = '<i>Hey</i> <b>there</b>!'; | ||
return { text, parse_mode: 'HTML' }; | ||
}); | ||
``` | ||
## Can the menu body be some media? | ||
### Can the menu body be some media? | ||
The menu body can be an object containing `media` and `type` for media. | ||
The `media` and `type` is the same as [Telegrams InputMedia](https://core.telegram.org/bots/api#inputmedia). | ||
The `media` and `type` is the same as [Telegrams `InputMedia`](https://core.telegram.org/bots/api#inputmedia). | ||
The media is just passed to grammY so check its documentation on [how to work with files](https://grammy.dev/guide/files.html). | ||
@@ -183,8 +184,8 @@ | ||
media: { | ||
source: `./${ctx.from.id}.jpg` | ||
source: `./${ctx.from.id}.jpg`, | ||
}, | ||
text: 'Some *caption*', | ||
parse_mode: 'Markdown' | ||
} | ||
}) | ||
parse_mode: 'Markdown', | ||
}; | ||
}); | ||
``` | ||
@@ -196,14 +197,15 @@ | ||
## How can I run a simple method when pressing a button? | ||
### How can I run a simple method when pressing a button? | ||
```ts | ||
menuTemplate.interact('Text', 'unique', { | ||
do: async ctx => { | ||
await ctx.answerCallbackQuery('yaay') | ||
return false | ||
} | ||
}) | ||
menuTemplate.interact('unique', { | ||
text: 'Text', | ||
do: async (ctx) => { | ||
await ctx.answerCallbackQuery('yaay'); | ||
return false; | ||
}, | ||
}); | ||
``` | ||
## Why do I have to return a boolean or string for the do/set function? | ||
### Why do I have to return a boolean or string for the do/set function? | ||
@@ -221,11 +223,12 @@ You can control if you want to update the menu afterwards or not. | ||
```ts | ||
menuTemplate.interact('Text', 'unique', { | ||
do: async ctx => { | ||
await ctx.answerCallbackQuery('go to parent menu after doing some logic') | ||
return '..' | ||
} | ||
}) | ||
menuTemplate.interact('unique', { | ||
text: 'Text', | ||
do: async (ctx) => { | ||
await ctx.answerCallbackQuery('go to parent menu after doing some logic'); | ||
return '..'; | ||
}, | ||
}); | ||
``` | ||
## How to use a dynamic text of a button? | ||
### How to use a dynamic text of a button? | ||
@@ -237,17 +240,18 @@ This is often required when translating ([i18n](https://grammy.dev/plugins/i18n.html)) your bot. | ||
```ts | ||
menuTemplate.interact(ctx => ctx.i18n.t('button'), 'unique', { | ||
do: async ctx => { | ||
await ctx.answerCallbackQuery(ctx.i18n.t('reponse')) | ||
return '.' | ||
} | ||
}) | ||
menuTemplate.interact('unique', { | ||
text: (ctx) => ctx.i18n.t('button'), | ||
do: async (ctx) => { | ||
await ctx.answerCallbackQuery(ctx.i18n.t('reponse')); | ||
return '.'; | ||
}, | ||
}); | ||
``` | ||
## How can I show a URL button? | ||
### How can I show a URL button? | ||
```ts | ||
menuTemplate.url('Text', 'https://edjopato.de') | ||
menuTemplate.url({ text: 'Text', url: 'https://edjopato.de' }); | ||
``` | ||
## How can I display two buttons in the same row? | ||
### How can I display two buttons in the same row? | ||
@@ -257,68 +261,74 @@ Use `joinLastRow` in the second button | ||
```ts | ||
menuTemplate.interact('Text', 'unique', { | ||
do: async ctx => { | ||
await ctx.answerCallbackQuery('yaay') | ||
return false | ||
} | ||
}) | ||
menuTemplate.interact('unique', { | ||
text: 'First', | ||
do: async (ctx) => { | ||
await ctx.answerCallbackQuery('yaay'); | ||
return false; | ||
}, | ||
}); | ||
menuTemplate.interact('Text', 'unique', { | ||
menuTemplate.interact('unique', { | ||
joinLastRow: true, | ||
do: async ctx => { | ||
await ctx.answerCallbackQuery('yaay') | ||
return false | ||
} | ||
}) | ||
text: 'Second', | ||
do: async (ctx) => { | ||
await ctx.answerCallbackQuery('yaay'); | ||
return false; | ||
}, | ||
}); | ||
``` | ||
## How can I toggle a value easily? | ||
### How can I toggle a value easily? | ||
```ts | ||
menuTemplate.toggle('Text', 'unique', { | ||
isSet: ctx => ctx.session.isFunny, | ||
menuTemplate.toggle('unique', { | ||
text: 'Text', | ||
isSet: (ctx) => ctx.session.isFunny, | ||
set: (ctx, newState) => { | ||
ctx.session.isFunny = newState | ||
return true | ||
} | ||
}) | ||
ctx.session.isFunny = newState; | ||
return true; | ||
}, | ||
}); | ||
``` | ||
## How can I select one of many values? | ||
### How can I select one of many values? | ||
```ts | ||
menuTemplate.select('unique', ['human', 'bird'], { | ||
menuTemplate.select('unique', { | ||
choices: ['human', 'bird'], | ||
isSet: (ctx, key) => ctx.session.choice === key, | ||
set: (ctx, key) => { | ||
ctx.session.choice = key | ||
return true | ||
} | ||
}) | ||
ctx.session.choice = key; | ||
return true; | ||
}, | ||
}); | ||
``` | ||
## How can I toggle many values? | ||
### How can I toggle many values? | ||
```ts | ||
menuTemplate.select('unique', ['has arms', 'has legs', 'has eyes', 'has wings'], { | ||
menuTemplate.select('unique', { | ||
showFalseEmoji: true, | ||
choices: ['has arms', 'has legs', 'has eyes', 'has wings'], | ||
isSet: (ctx, key) => Boolean(ctx.session.bodyparts[key]), | ||
set: (ctx, key, newState) => { | ||
ctx.session.bodyparts[key] = newState | ||
return true | ||
} | ||
}) | ||
ctx.session.bodyparts[key] = newState; | ||
return true; | ||
}, | ||
}); | ||
``` | ||
## How can I interact with many values based on the pressed button? | ||
### How can I interact with many values based on the pressed button? | ||
```ts | ||
menuTemplate.choose('unique', ['walk', 'swim'], { | ||
menuTemplate.choose('unique', { | ||
choices: ['walk', 'swim'], | ||
do: async (ctx, key) => { | ||
await ctx.answerCallbackQuery(`Lets ${key}`) | ||
await ctx.answerCallbackQuery(`Lets ${key}`); | ||
// You can also go back to the parent menu afterwards for some 'quick' interactions in submenus | ||
return '..' | ||
} | ||
}) | ||
return '..'; | ||
}, | ||
}); | ||
``` | ||
## What's the difference between choose and select? | ||
### What's the difference between choose and select? | ||
@@ -331,3 +341,3 @@ If you want to do something based on the choice, use `menuTemplate.choose(…)`. | ||
## How can I use dynamic text for many values with choose or select? | ||
### How can I use dynamic text for many values with choose or select? | ||
@@ -343,3 +353,3 @@ One way of doing so is via `Record<string, string>` as input for the choices: | ||
menuTemplate.choose('unique', choices, …) | ||
menuTemplate.choose('unique', {choices, …}) | ||
``` | ||
@@ -350,3 +360,4 @@ | ||
```ts | ||
menuTemplate.choose('unique', ['a', 'b'], { | ||
menuTemplate.choose('unique', { | ||
choices: ['a', 'b'], | ||
do: …, | ||
@@ -359,3 +370,3 @@ buttonText: (context, text) => { | ||
## I have too much content for one message. Can I use a pagination? | ||
### I have too much content for one message. Can I use a pagination? | ||
@@ -370,10 +381,10 @@ `menuTemplate.pagination` is basically a glorified `choose`. | ||
getTotalPages: () => 42, | ||
getCurrentPage: context => context.session.page, | ||
getCurrentPage: (context) => context.session.page, | ||
setPage: (context, page) => { | ||
context.session.page = page | ||
} | ||
}) | ||
context.session.page = page; | ||
}, | ||
}); | ||
``` | ||
## My choose/select has too many choices. Can I use a pagination? | ||
### My choose/select has too many choices. Can I use a pagination? | ||
@@ -383,8 +394,9 @@ When you don't use a pagination, you might have noticed that not all of your choices are displayed. | ||
You can select the amount of rows and columns via `maxRows` and `columns`. | ||
The pagination works similar to `menuTemplate.pagination` but you do not need to supply the amount of total pages as this is calculated from your choices. | ||
The pagination works similar to `menuTemplate.pagination`, but you do not need to supply the amount of total pages as this is calculated from your choices. | ||
```ts | ||
menuTemplate.choose('eat', ['cheese', 'bread', 'salad', 'tree', …], { | ||
menuTemplate.choose('eat', { | ||
columns: 1, | ||
maxRows: 2, | ||
choices: ['cheese', 'bread', 'salad', 'tree', …], | ||
getCurrentPage: context => context.session.page, | ||
@@ -397,31 +409,40 @@ setPage: (context, page) => { | ||
## How can I use a submenu? | ||
### How can I use a submenu? | ||
```ts | ||
const submenuTemplate = new MenuTemplate<MyContext>('I am a submenu') | ||
submenuTemplate.interact('Text', 'unique', { | ||
do: async ctx => ctx.answerCallbackQuery('You hit a button in a submenu') | ||
}) | ||
submenuTemplate.manualRow(createBackMainMenuButtons()) | ||
const submenuTemplate = new MenuTemplate<MyContext>('I am a submenu'); | ||
submenuTemplate.interact('unique', { | ||
text: 'Text', | ||
do: async (ctx) => ctx.answerCallbackQuery('You hit a button in a submenu'), | ||
}); | ||
submenuTemplate.manualRow(createBackMainMenuButtons()); | ||
menuTemplate.submenu('Text', 'unique', submenuTemplate) | ||
menuTemplate.submenu('unique', submenuTemplate, { text: 'Text' }); | ||
``` | ||
## How can I use a submenu with many choices? | ||
### How can I use a submenu with many choices? | ||
```ts | ||
const submenuTemplate = new MenuTemplate<MyContext>(ctx => `You chose city ${ctx.match[1]}`) | ||
submenuTemplate.interact('Text', 'unique', { | ||
do: async ctx => { | ||
console.log('Take a look at ctx.match. It contains the chosen city', ctx.match) | ||
await ctx.answerCallbackQuery('You hit a button in a submenu') | ||
return false | ||
} | ||
}) | ||
submenuTemplate.manualRow(createBackMainMenuButtons()) | ||
const submenuTemplate = new MenuTemplate<MyContext>((ctx) => | ||
`You chose city ${ctx.match[1]}` | ||
); | ||
submenuTemplate.interact('unique', { | ||
text: 'Text', | ||
do: async (ctx) => { | ||
console.log( | ||
'Take a look at ctx.match. It contains the chosen city', | ||
ctx.match, | ||
); | ||
await ctx.answerCallbackQuery('You hit a button in a submenu'); | ||
return false; | ||
}, | ||
}); | ||
submenuTemplate.manualRow(createBackMainMenuButtons()); | ||
menuTemplate.chooseIntoSubmenu('unique', ['Gotham', 'Mos Eisley', 'Springfield'], submenuTemplate) | ||
menuTemplate.chooseIntoSubmenu('unique', submenuTemplate, { | ||
choices: ['Gotham', 'Mos Eisley', 'Springfield'], | ||
}); | ||
``` | ||
## Can I close the menu? | ||
### Can I close the menu? | ||
@@ -436,12 +457,13 @@ You can delete the message like you would do with grammY: `ctx.deleteMessage()`. | ||
```ts | ||
menuTemplate.interact('Delete the menu', 'unique', { | ||
do: async context => { | ||
await deleteMenuFromContext(context) | ||
menuTemplate.interact('unique', { | ||
text: 'Delete the menu', | ||
do: async (context) => { | ||
await deleteMenuFromContext(context); | ||
// Make sure not to try to update the menu afterwards. You just deleted it and it would just fail to update a missing message. | ||
return false | ||
} | ||
}) | ||
return false; | ||
}, | ||
}); | ||
``` | ||
## Can I send the menu manually? | ||
### Can I send the menu manually? | ||
@@ -451,4 +473,4 @@ If you want to send the root menu use `ctx => menuMiddleware.replyToContext(ctx)` | ||
```ts | ||
const menuMiddleware = new MenuMiddleware('/', menuTemplate) | ||
bot.command('start', ctx => menuMiddleware.replyToContext(ctx)) | ||
const menuMiddleware = new MenuMiddleware('/', menuTemplate); | ||
bot.command('start', (ctx) => menuMiddleware.replyToContext(ctx)); | ||
``` | ||
@@ -460,15 +482,18 @@ | ||
```ts | ||
const menuMiddleware = new MenuMiddleware('/', menuTemplate) | ||
bot.command('start', ctx => menuMiddleware.replyToContext(ctx, path)) | ||
const menuMiddleware = new MenuMiddleware('/', menuTemplate); | ||
bot.command('start', (ctx) => menuMiddleware.replyToContext(ctx, path)); | ||
``` | ||
You can also use sendMenu functions like `replyMenuToContext` to send a menu manually. | ||
You can also use `sendMenu` functions like `replyMenuToContext` to send a menu manually. | ||
```ts | ||
import {MenuTemplate, replyMenuToContext} from 'grammy-inline-menu' | ||
const settingsMenu = new MenuTemplate('Settings') | ||
bot.command('settings', async ctx => replyMenuToContext(settingsMenu, ctx, '/settings/')) | ||
import { MenuTemplate, replyMenuToContext } from 'grammy-inline-menu'; | ||
const settingsMenu = new MenuTemplate('Settings'); | ||
bot.command( | ||
'settings', | ||
async (ctx) => replyMenuToContext(settingsMenu, ctx, '/settings/'), | ||
); | ||
``` | ||
## Can I send the menu from external events? | ||
### Can I send the menu from external events? | ||
@@ -480,42 +505,50 @@ When sending from external events you still have to supply the context to the message or some parts of your menu might not work as expected! | ||
```ts | ||
const sendMenuFunction = generateSendMenuToChatFunction(bot.telegram, menu, '/settings/') | ||
const sendMenuFunction = generateSendMenuToChatFunction( | ||
bot.telegram, | ||
menu, | ||
'/settings/', | ||
); | ||
async function externalEventOccured() { | ||
await sendMenuFunction(userId, context) | ||
await sendMenuFunction(userId, context); | ||
} | ||
``` | ||
## Didn't this menu had a question function? | ||
### Didn't this menu had a question function? | ||
Yes. It was moved into a separate library with version 5 as it made the source code overly complicated. | ||
When you want to use it check [grammy-stateless-question](https://github.com/grammyjs/stateless-question). | ||
When you want to use it check [`grammy-stateless-question`](https://github.com/grammyjs/stateless-question). | ||
```ts | ||
import {getMenuOfPath} from 'grammy-inline-menu' | ||
import { getMenuOfPath } from 'grammy-inline-menu'; | ||
const myQuestion = new TelegrafStatelessQuestion<MyContext>('unique', async (context, additionalState) => { | ||
const answer = context.message.text | ||
console.log('user responded with', answer) | ||
await replyMenuToContext(menuTemplate, context, additionalState) | ||
}) | ||
const myQuestion = new TelegrafStatelessQuestion<MyContext>( | ||
'unique', | ||
async (context, additionalState) => { | ||
const answer = context.message.text; | ||
console.log('user responded with', answer); | ||
await replyMenuToContext(menuTemplate, context, additionalState); | ||
}, | ||
); | ||
bot.use(myQuestion.middleware()) | ||
bot.use(myQuestion.middleware()); | ||
menuTemplate.interact('Question', 'unique', { | ||
menuTemplate.interact('unique', { | ||
text: 'Question', | ||
do: async (context, path) => { | ||
const text = 'Tell me the answer to the world and everything.' | ||
const additionalState = getMenuOfPath(path) | ||
await myQuestion.replyWithMarkdown(context, text, additionalState) | ||
return false | ||
} | ||
}) | ||
const text = 'Tell me the answer to the world and everything.'; | ||
const additionalState = getMenuOfPath(path); | ||
await myQuestion.replyWithMarkdown(context, text, additionalState); | ||
return false; | ||
}, | ||
}); | ||
``` | ||
# Documentation | ||
## Documentation | ||
The methods should have explaining documentation by itself. | ||
Also, there should be multiple @example entries in the docs to see different ways of using the method. | ||
Also, there should be multiple `@example` entries in the docs to see different ways of using the method. | ||
If you think the jsdoc / README can be improved just go ahead and create a Pull Request. | ||
If you think the JSDoc / README can be improved just go ahead and create a Pull Request. | ||
Let's improve things together! |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
7
533
Yes
164666
2118
+ Addedtype-fest@4.30.2(transitive)
- Removedtype-fest@3.13.1(transitive)
Updatedtype-fest@^4.0.0