This commit is contained in:
2024-02-07 14:46:20 +04:00
parent 820a683319
commit dedc65ea7c
11 changed files with 555 additions and 2 deletions
+31
View File
@@ -0,0 +1,31 @@
export class Editor {
/**
* Порядок отображения кнопок формы ответа.
*/
buttons: string[] = null;
/**
* Массив из 2 значений, которые являются HTML-кодами, что будут добавлены до и после списка кнопок.
* @type {string[]}
*/
wrapper: string[] = ['<table><tr>', '</tr></table>'];
/**
* Метод для генерации кнопок.
* @param {string} key Название кода, ключ объекта `FORUM.editor`.
* @param value Набор правил кода, соответствующее ключу key значения объекта `FORUM.editor[key]`.
*/
generator(key: string, value: any): string {
return `<td class="form-buttons-button" id="button-${key}" title="${value.name}"><img onclick="FORUM.get(&quot;editor.${key}.onclick()&quot;)" src="/i/blank.gif" alt="&nbsp;" /></td>`;
}
/**
* Дополнительные элементы, которые могут быть добавлены наряду с другими формы ответа
* @type {Object.<string, string>}
*/
specials: {[key: string] : string} = {
'|': '<td class="form-buttons-splitter">&nbsp;</td>',
'~': '<td class="form-buttons-filler">&nbsp;</td>',
'\n': '</tr><tr>',
};
}
+5
View File
@@ -0,0 +1,5 @@
import MybbCode from "./mybb-code";
(window as any).MyBBCode = (window as any).MyBBCode || (() => {
return new MybbCode();
})();
+260
View File
@@ -0,0 +1,260 @@
import {AdapterCallback, AuthorEditorCallback, EventResolver, TagCallback} from "./types";
import {FORUM, MybbEditorTag} from "./mybb-editor";
import {Editor} from "./editor";
export default class MybbCode {
/**
* Текст, который будет установлен при вызове `set_html(false)` в обработчиках тегов,
* добавленных при помощи метода `addTag`.
* @type loadingText {string}
* @see addTag
*/
loadingText = '\u0421\u043E\u0434\u0435\u0440\u0436\u0438\u043C\u043E\u0435 \u0437\u0430\u0433\u0440\u0443\u0436\u0430\u0435\u0442\u0441\u044F...';
/**
* Набор тегов
*/
private tags: {[key: string]: TagCallback} = {};
/**
* Набор адаптеров
*/
private adapters: AdapterCallback[] = [];
/**
* Набор обработчиков столбца автора
*/
private authorEditors: AuthorEditorCallback[] = [];
/**
* Выделение контейнеров сообщений для событий
*/
events: {[key: string]: EventResolver} = {
'pun_preview': (_) => $('#post-preview .post-content'),
'pun_post': (_) => $('.topic .new-ajax .post-content'),
'pun_edit': (e) => $(e.sender),
'pun_main_ready': (_) => $('.post-content'),
'spoiler.firstOpen': (e) => $(e.sender).parent().find('blockquote').eq(0)
};
/**
* Набор правил для генерации кнопок формы ответа. Удаление полей настоятельно не рекомендуется.
*/
editor: Editor = new Editor();
/**
* Конструктор класса `MyBBCode`.
*/
constructor() {
$(document).on(Object.keys(this.events).join(' '), (event) => {
const blocks = this.events[event.type](event);
this.processPostContents(blocks);
});
if (FORUM.editor) {
this.editor.buttons = Object.keys(FORUM.editor).filter((v) => v !== 'settings');
$(document).on('pun_main_ready', () => this.generateEditorButtons());
}
}
/**
* Добавление нового события, который будет запускать обработчики сообщений и обработчики пользовательских кодов.
* @param {string} name Отслеживаемое DOM-событие, используемое методом `jQuery.on(name)`, и которое можно
* вызвать методом `jQuery.trigger(name)`.
* @param {EventResolver} resolver Это должна быть функция, которые на основе объекта события (`event`, он
* подаётся на вход этой функции), определяет, какие контейнеры должны быть
* обработаны обработчиками.
*/
registerEvent(name: string, resolver: EventResolver): void {
$(document).on(name, (event) => this.processPostContents(resolver(event)));
}
/**
* Установка HTML загружающегося тега.
* @param {JQuery} elem Цель установки
* @param {string|false} html HTML-код для установки
*/
private setHtml(elem: JQuery, html: string|false): void {
if (html === false) {
elem.html(this.loadingText).addClass('loading');
}
else {
elem.html(html).removeClass('loading');
}
}
/**
* Обработка контейнера сообщения всеми зарегистрированными обработчиками.
* @param containers jQuery-объект, ссылающийся на контейнер (или контейнеры), который содержит необработанное сообщение.
*/
private processPostContents(containers: JQuery): void {
containers.each((_, container) => {
const $container = $(container);
$.each(this.tags, (tag, action) => {
$('.custom_tag_' + tag, $container).each((i, elem) => {
const $elem = $(elem);
let id = $elem.prop('id') || (tag + '-' + i);
if ($elem.hasClass('processed')) {
return;
}
action($elem, '.custom_tag_' + tag + '#' + id, this.setHtml.bind(this, $elem));
$elem.addClass('processed');
});
});
$.each(this.adapters, (i, action) => {
action($container);
});
if ($container.hasClass('post-content')) {
const ul = $container.parents('.post').find('.post-author ul');
if (!ul.hasClass('mybbcode-processed')) {
$.each(this.authorEditors, (i, action) => {
action(ul);
});
ul.addClass('mybbcode-processed')
}
}
});
}
/**
* Добавление обработчиков пользовательских тегов, добавленных через систему пользовательских тегов
* (/admin_forms.php — "Пользовательские bb-теги"). Для корректной работы обработчика рекомендуется
* (но НЕ необходимо) указать для тега флаг `u` (unique).
* @param {string} name Строка, название тега. Должно совпадать с названием тега в поле "Пользовательские bb-теги".
* @param {TagCallback} callback Функция, которая будет вызываться по событию.
*/
addTag(name: string, callback: TagCallback): void {
if (this.tags[name]) {
console.warn('[MyBBCode] Tag "' + name + '" declared more than one time!');
}
this.tags[name] = callback;
}
/**
* Добавление общих обработчиков, которые будут применяться к сообщениям.
* @param {AdapterCallback} callback
*/
addAdapter(callback: AdapterCallback): void {
this.adapters.push(callback);
}
/**
* Добавление обработчика, изменяющего столбец автора сообщения.
* После обработки, к ul-элементу будет добавлен класс `mybbcode-processed`; если элемент имеет этот класс,
* обработчик не будет применён.
* @param {AuthorEditorCallback} callback
*/
addAuthorEditor(callback: AuthorEditorCallback): void {
this.authorEditors.push(callback);
}
/**
* Сгенерировать кнопку
* @param {string} key id кнопки
* @returns {string} Код кнопки
*/
private generateEditorButton(key: string): string {
if (!FORUM.editor) {
return '';
}
if (key in this.editor.specials) {
return this.editor.specials[key]
}
if (key in FORUM.editor) {
const value = FORUM.editor[key];
return this.editor.generator(key, value);
}
return '';
}
/**
* Сгенерировать заново строку с кнопками редактора.
*/
generateEditorButtons(): void {
if (!FORUM.editor || (this.editor.buttons === null)) {
return;
}
const old = Object.keys(FORUM.editor).filter((v) => v !== 'settings');
let changed = old.length !== this.editor.buttons.length;
for (let i = 0; !changed && (i < old.length); i++) {
changed = changed || (old[i] !== this.editor.buttons[i]);
}
if (!changed) {
return;
}
let result = this.editor.wrapper[0];
for (const key of this.editor.buttons) {
result += this.generateEditorButton(key);
}
result += this.editor.wrapper[1];
$('#form-buttons').empty().html(result);
}
/**
* Добавление кнопки формы.
* После объявления при помощи этого метода, объект `data` будет доступен после объявления как `FORUM.editor.key`.
* @param {string} key Идентификатор кнопки, который будет добавлен к `td.button#button-key`.
* @param {MybbEditorTag} data Данные поля, которые будут использоваться стандартными скриптами.
* Этому объектов можно указать и другие поля, однако указанные обязательны.
* @param {string} data.name Строка, название кнопки.
* @param {function} data.onclick Функция, которая будет вызвана при нажатии кнопки.
* @param {number|string|false} [before] Указание, перед каким элементом необходимо добавить кнопку. Допустимые значения:
* Число `i` — тогда кнопка будет добавлена перед `i`-м элементом массива `MyBBCode.editor.buttons`.
* Строкой-идентификатором кнопки `key` — тогда новая кнопка будет помещена перед кнопкой с идентификатором `key`.
*/
addEditorButton(key: string, data: MybbEditorTag, before: number|string|false = false): void {
if (!('FORUM' in window) || !('editor' in FORUM)) {
return null;
}
if (!(key in this.editor.specials)) {
FORUM.editor[key] = data;
}
if (this.editor.buttons !== null) {
let index: number|false;
if (before === false) {
index = false;
}
else if (typeof before === "string") {
index = this.editor.buttons.indexOf(before);
index = (index < 0) ? false : index;
}
else if ((typeof before === "number") && (before >= 0) && (before < this.editor.buttons.length)) {
index = before;
}
else {
index = false;
}
if (index === false) {
this.editor.buttons.push(key);
}
else {
this.editor.buttons.splice(index, 0, key);
}
}
}
}
+66
View File
@@ -0,0 +1,66 @@
declare module 'FORUM' {
let FORUM: {
editor: MybbEditor
}
export class MybbEditorTag {
/**
* @param {string} name Строка, название кнопки.
*/
name: string;
/**
* @param {function} onclick Функция, которая будет вызвана при нажатии кнопки.
*/
onclick: () => void;
}
export type MybbEditorSettings = {
autofocus: string
}
export type MybbEditor = {
settings: MybbEditorSettings;
[key: string]: MybbEditorTag | MybbEditorSettings;
}
export function quote(e: string, t: string, i: string): void;
export function bbcode(tagStart: string, tagEnd: string): void;
export function changeVisibility(areaName: string): void;
export function insert(value: string): void;
export function smile(emote: string): void;
export function tag_table(rows: number, columns: number): void;
export function MYBB_vsi(element: HTMLElement, event: any): void;
export function insertUploadedImage(element: HTMLElement | JQuery): void;
export function tag_spoiler(spoilerType: string): void;
export function keyboard(character: string): void;
export function to(target: string): void;
export class MYBB_vsc {
cH: any;
RG: { [key: string]: MYBB_vsc_item };
checkHost: (hostingItem: MYBB_vsc_item, input: string) => boolean;
clickHost: (element: HTMLAnchorElement) => void;
parse: () => void;
isLink: (input: string) => boolean;
message: (html: string) => void;
reInit: () => void;
}
type MYBB_vsc_item = {
l: "0" | "1";
t: string;
x: RegExp;
nf: string;
}
}
+10
View File
@@ -0,0 +1,10 @@
export type AdapterCallback = (container: JQuery) => void;
export type TagCallback = (elem: JQuery,
selector: string,
set_html: string | boolean) => void;
export type AuthorEditorCallback = (container: JQuery) => void;
export type EventResolver = (e: any) => JQuery