diff --git a/extensions/embed/js/src/forum/index.js b/extensions/embed/js/src/forum/index.js
index f858688120..75a6290897 100644
--- a/extensions/embed/js/src/forum/index.js
+++ b/extensions/embed/js/src/forum/index.js
@@ -36,7 +36,7 @@ app.pageInfo = Stream({});
const reposition = function () {
const info = app.pageInfo();
- this.$().css('top', Math.max(0, info.scrollTop - info.offsetTop));
+ this.element.style.top = Math.max(0, info.scrollTop - info.offsetTop) + 'px';
};
extend(ModalManager.prototype, 'show', reposition);
@@ -50,7 +50,7 @@ window.iFrameResizer = {
extend('flarum/forum/components/PostStream', 'goToNumber', function (promise, number) {
if (number === 'reply' && 'parentIFrame' in window && app.composer.isFullScreen()) {
- const itemTop = this.$('.PostStream-item:last').offset().top;
+ const itemTop = this.element.getBoundingClientRect().top + document.documentElement.scrollTop;
window.parentIFrame.scrollToOffset(0, itemTop);
}
});
diff --git a/extensions/emoji/js/src/forum/addComposerAutocomplete.js b/extensions/emoji/js/src/forum/addComposerAutocomplete.js
index 7d6df93903..120341bd74 100644
--- a/extensions/emoji/js/src/forum/addComposerAutocomplete.js
+++ b/extensions/emoji/js/src/forum/addComposerAutocomplete.js
@@ -24,7 +24,8 @@ export default function addComposerAutocomplete() {
extend('flarum/common/components/TextEditor', 'onbuild', function () {
this.emojiDropdown = new AutocompleteDropdown();
- const $editor = this.$('.TextEditor-editor').wrap('
');
+ const editor = this.element.querySelector('.TextEditor-editor');
+ editor.outerHTML = `${editor.outerHTML}
`;
this.navigator = new KeyboardNavigatable();
this.navigator
@@ -33,9 +34,9 @@ export default function addComposerAutocomplete() {
.onDown(() => this.emojiDropdown.navigate(1))
.onSelect(this.emojiDropdown.complete.bind(this.emojiDropdown))
.onCancel(this.emojiDropdown.hide.bind(this.emojiDropdown))
- .bindTo($editor);
+ .bindTo(editor);
- $editor.after($(''));
+ editor.outerHTML = editor.outerHTML + '';
});
extend('flarum/common/components/TextEditor', 'buildEditorParams', function (params) {
@@ -134,27 +135,28 @@ export default function addComposerAutocomplete() {
if (suggestions.length) {
this.emojiDropdown.items = suggestions;
- m.render(this.$('.ComposerBody-emojiDropdownContainer')[0], this.emojiDropdown.render());
+ m.render(this.element.querySelector('.ComposerBody-emojiDropdownContainer'), this.emojiDropdown.render());
this.emojiDropdown.show();
const coordinates = this.attrs.composer.editor.getCaretCoordinates(autocompleting.absoluteStart);
- const width = this.emojiDropdown.$().outerWidth();
- const height = this.emojiDropdown.$().outerHeight();
- const parent = this.emojiDropdown.$().offsetParent();
+ const rect = this.emojiDropdown.element.getBoundingClientRect();
+ const width = rect.width;
+ const height = rect.height;
+ const parent = this.emojiDropdown.element.offsetParent;
let left = coordinates.left;
let top = coordinates.top + 15;
// Keep the dropdown inside the editor.
- if (top + height > parent.height()) {
+ if (top + height > parent.clientHeight) {
top = coordinates.top - height - 15;
}
- if (left + width > parent.width()) {
- left = parent.width() - width;
+ if (left + width > parent.clientWidth) {
+ left = parent.clientWidth - width;
}
// Prevent the dropdown from going off screen on mobile
- top = Math.max(-(parent.offset().top - $(document).scrollTop()), top);
- left = Math.max(-parent.offset().left, left);
+ top = Math.max(-(parent.getBoundingClientRect().top), top);
+ left = Math.max(-parent.getBoundingClientRect().left + document.documentElement.scrollLeft, left);
this.emojiDropdown.show(left, top);
}
@@ -163,7 +165,7 @@ export default function addComposerAutocomplete() {
buildSuggestions();
this.emojiDropdown.setIndex(0);
- this.emojiDropdown.$().scrollTop(0);
+ this.emojiDropdown.element.scrollTo({ top: 0 });
this.emojiDropdown.active = true;
}
});
diff --git a/extensions/emoji/js/src/forum/fragments/AutocompleteDropdown.js b/extensions/emoji/js/src/forum/fragments/AutocompleteDropdown.js
index e4f27921e3..086b56821d 100644
--- a/extensions/emoji/js/src/forum/fragments/AutocompleteDropdown.js
+++ b/extensions/emoji/js/src/forum/fragments/AutocompleteDropdown.js
@@ -18,17 +18,15 @@ export default class AutocompleteDropdown extends Fragment {
}
show(left, top) {
- this.$()
- .show()
- .css({
- left: left + 'px',
- top: top + 'px',
- });
+ const style = this.element.style;
+ style.display = 'block';
+ style.left = left + 'px';
+ style.top = top + 'px';
this.active = true;
}
hide() {
- this.$().hide();
+ this.element.style.display = 'none';
this.active = false;
}
@@ -40,42 +38,51 @@ export default class AutocompleteDropdown extends Fragment {
}
complete() {
- this.$('li:not(.Dropdown-header)').eq(this.index).find('button').click();
+ this.element.querySelectorAll('li:not(.Dropdown-header)')[this.index].querySelector('button').click();
}
+ // todo: check if copied implementation matches the original behavior
setIndex(index, scrollToItem) {
if (this.keyWasJustPressed && !scrollToItem) return;
- const $dropdown = this.$();
- const $items = $dropdown.find('li:not(.Dropdown-header)');
+ const dropdown = this.element;
+ const items = dropdown.querySelectorAll('li:not(.Dropdown-header)');
let rangedIndex = index;
if (rangedIndex < 0) {
- rangedIndex = $items.length - 1;
- } else if (rangedIndex >= $items.length) {
+ rangedIndex = items.length - 1;
+ } else if (rangedIndex >= items.length) {
rangedIndex = 0;
}
this.index = rangedIndex;
- const $item = $items.removeClass('active').eq(rangedIndex).addClass('active');
+ items.forEach((el) => el.classList.remove('active'));
+ const item = items[rangedIndex];
+ item.classList.add('active');
if (scrollToItem) {
- const dropdownScroll = $dropdown.scrollTop();
- const dropdownTop = $dropdown.offset().top;
- const dropdownBottom = dropdownTop + $dropdown.outerHeight();
- const itemTop = $item.offset().top;
- const itemBottom = itemTop + $item.outerHeight();
+ const documentScrollTop = document.documentElement.scrollTop;
+ const dropdownScroll = dropdown.scrollTop;
+ const dropdownRect = dropdown.getBoundingClientRect();
+ const dropdownTop = dropdownRect.top + documentScrollTop;
+ const dropdownBottom = dropdownTop + dropdownRect.height;
+ const itemRect = item.getBoundingClientRect();
+ const itemTop = itemRect.top + documentScrollTop;
+ const itemBottom = itemTop + itemRect.height;
let scrollTop;
if (itemTop < dropdownTop) {
- scrollTop = dropdownScroll - dropdownTop + itemTop - parseInt($dropdown.css('padding-top'), 10);
+ scrollTop = dropdownScroll - dropdownTop + itemTop - parseInt(getComputedStyle(dropdown).paddingTop, 10);
} else if (itemBottom > dropdownBottom) {
- scrollTop = dropdownScroll - dropdownBottom + itemBottom + parseInt($dropdown.css('padding-bottom'), 10);
+ scrollTop = dropdownScroll - dropdownBottom + itemBottom + parseInt(getComputedStyle(dropdown).paddingBottom, 10);
}
if (typeof scrollTop !== 'undefined') {
- $dropdown.stop(true).animate({ scrollTop }, 100);
+ dropdown.scrollTo({
+ top: scrollTop,
+ behavior: 'smooth',
+ });
}
}
}
diff --git a/framework/core/js/package.json b/framework/core/js/package.json
index 249a53a55a..91146c9bfd 100644
--- a/framework/core/js/package.json
+++ b/framework/core/js/package.json
@@ -5,15 +5,15 @@
"type": "module",
"prettier": "@flarum/prettier-config",
"dependencies": {
+ "@popperjs/core": "^2.11.8",
"body-scroll-lock": "^4.0.0-beta.0",
- "bootstrap": "^3.4.1",
+ "bootstrap": "^5.3.3",
"clsx": "^1.1.1",
"color-thief-browser": "^2.0.2",
"dayjs": "^1.10.7",
"focus-trap": "^6.7.1",
"format-message": "^6.2.4",
"jquery": "^3.6.0",
- "jquery.hotkeys": "^0.1.0",
"mithril": "^2.2",
"nanoid": "^3.1.30",
"punycode": "^2.1.1",
@@ -24,7 +24,7 @@
"@flarum/jest-config": "^1.0.0",
"@flarum/prettier-config": "^1.0.0",
"@types/body-scroll-lock": "^3.1.0",
- "@types/jquery": "^3.5.10",
+ "@types/bootstrap": "^5.2.10",
"@types/mithril": "^2.0.8",
"@types/punycode": "^2.1.0",
"@types/textarea-caret": "^3.0.1",
diff --git a/framework/core/js/src/@types/global.d.ts b/framework/core/js/src/@types/global.d.ts
index 0e10ddecb2..b722e52cb6 100644
--- a/framework/core/js/src/@types/global.d.ts
+++ b/framework/core/js/src/@types/global.d.ts
@@ -56,6 +56,14 @@ declare type VnodeElementTag, C extends Componen
*/
declare const app: import('../common/Application').default;
+/**
+ * @deprecated We are moving away from jQuery.
+ */
+declare const $: JQueryStatic;
+/**
+ * @deprecated We are moving away from jQuery.
+ */
+declare const jQuery: JQueryStatic;
declare const m: import('mithril').Static;
declare const dayjs: typeof import('dayjs');
diff --git a/framework/core/js/src/admin/components/AdminNav.js b/framework/core/js/src/admin/components/AdminNav.js
index 98e4fd8784..5f3c7e824e 100644
--- a/framework/core/js/src/admin/components/AdminNav.js
+++ b/framework/core/js/src/admin/components/AdminNav.js
@@ -37,20 +37,17 @@ export default class AdminNav extends Component {
}
scrollToActive() {
- const children = $('.Dropdown-menu').children('.active');
- const nav = $('#admin-navigation');
- const time = app.previous.type ? 250 : 0;
+ const children = document.querySelectorAll('.Dropdown-menu > .active');
+ const nav = document.getElementById('admin-navigation');
if (
children.length > 0 &&
- (children[0].offsetTop > nav.scrollTop() + nav.outerHeight() || children[0].offsetTop + children[0].offsetHeight < nav.scrollTop())
+ (children[0].offsetTop > nav.scrollTop + nav.getBoundingClientRect().height || children[0].offsetTop + children[0].offsetHeight < nav.scrollTop)
) {
- nav.animate(
- {
- scrollTop: children[0].offsetTop - nav.height() / 2,
- },
- time
- );
+ nav.scrollTo({
+ top: children[0].offsetTop - nav.clientHeight / 2,
+ behavior: app.previous.type ? 'smooth' : 'instant'
+ });
}
}
diff --git a/framework/core/js/src/admin/components/CreateUserModal.tsx b/framework/core/js/src/admin/components/CreateUserModal.tsx
index 3d358f21fc..6079edb776 100644
--- a/framework/core/js/src/admin/components/CreateUserModal.tsx
+++ b/framework/core/js/src/admin/components/CreateUserModal.tsx
@@ -199,7 +199,7 @@ export default class CreateUserModal {
function setEmailVisibility(visible: boolean) {
- // Get needed jQuery element refs
- const emailContainer = $(`[data-column-name=emailAddress][data-user-id=${user.id()}] .UserList-email`);
- const emailAddress = emailContainer.find('.UserList-emailAddress');
- const emailToggleButton = emailContainer.find('.UserList-emailIconBtn');
- const emailToggleButtonIcon = emailToggleButton.find('.icon');
+ // Get needed element refs
+ const emailContainer = document.querySelector(`[data-column-name='emailAddress'][data-user-id='${user.id()}'] .UserList-email`)!;
+ const emailAddress = emailContainer.querySelector('.UserList-emailAddress')!;
+ const emailToggleButton = emailContainer.querySelector('.UserList-emailIconBtn')!;
+ const emailToggleButtonIcon = emailToggleButton.querySelector('.icon')!;
- emailToggleButton.attr(
+ emailToggleButton.setAttribute(
'title',
extractText(
visible
@@ -318,23 +318,27 @@ export default class UserListPage extends AdminPage {
)
);
- emailAddress.attr('aria-hidden', visible ? null : 'true');
+ if (visible) {
+ emailAddress.removeAttribute('aria-hidden');
+ } else {
+ emailAddress.setAttribute('aria-hidden', 'true');
+ }
if (visible) {
- emailToggleButtonIcon.addClass('fa-eye');
- emailToggleButtonIcon.removeClass('fa-eye-slash');
+ emailToggleButtonIcon.classList.add('fa-eye');
+ emailToggleButtonIcon.classList.remove('fa-eye-slash');
} else {
- emailToggleButtonIcon.removeClass('fa-eye');
- emailToggleButtonIcon.addClass('fa-eye-slash');
+ emailToggleButtonIcon.classList.remove('fa-eye');
+ emailToggleButtonIcon.classList.add('fa-eye-slash');
}
// Need the string interpolation to prevent TS error.
- emailContainer.attr('data-email-shown', `${visible}`);
+ emailContainer.setAttribute('data-email-shown', `${visible}`);
}
function toggleEmailVisibility() {
- const emailContainer = $(`[data-column-name=emailAddress][data-user-id=${user.id()}] .UserList-email`);
- const emailShown = emailContainer.attr('data-email-shown') === 'true';
+ const emailContainer = document.querySelector(`[data-column-name='emailAddress'][data-user-id='${user.id()}'] .UserList-email`)!;
+ const emailShown = emailContainer.getAttribute('data-email-shown') === 'true';
if (emailShown) {
setEmailVisibility(false);
diff --git a/framework/core/js/src/common/Component.ts b/framework/core/js/src/common/Component.ts
index fdf244443e..90e3607912 100644
--- a/framework/core/js/src/common/Component.ts
+++ b/framework/core/js/src/common/Component.ts
@@ -108,6 +108,7 @@ export default abstract class Component;
diff --git a/framework/core/js/src/common/Fragment.ts b/framework/core/js/src/common/Fragment.ts
index 4d0f5416cb..c78835ddb0 100644
--- a/framework/core/js/src/common/Fragment.ts
+++ b/framework/core/js/src/common/Fragment.ts
@@ -33,6 +33,7 @@ export default abstract class Fragment {
* @param [selector] a jQuery-compatible selector string
* @returns the jQuery object for the DOM node
* @final
+ * @deprecated We are moving away from jQuery.
*/
public $(selector?: string): JQuery {
const $element = $(this.element) as JQuery;
diff --git a/framework/core/js/src/common/components/AbstractGlobalSearch.tsx b/framework/core/js/src/common/components/AbstractGlobalSearch.tsx
index ccc8614f8a..8e3e702acb 100644
--- a/framework/core/js/src/common/components/AbstractGlobalSearch.tsx
+++ b/framework/core/js/src/common/components/AbstractGlobalSearch.tsx
@@ -86,12 +86,17 @@ export default abstract class AbstractGlobalSearch;
const openSearchModal = () => {
- this.$('input').blur() &&
+ this.blur() &&
app.modal.show(() => import('../../common/components/SearchModal'), { searchState: this.searchState, sources: this.sourceItems().toArray() });
};
@@ -101,7 +106,7 @@ export default abstract class AbstractGlobalSearch {
- this.$('input').blur();
+ this.blur();
setTimeout(() => openSearchModal(), 150);
}}
>
@@ -124,7 +129,7 @@ export default abstract class AbstractGlobalSearch {
if (e.key === 'Enter') {
e.preventDefault();
- this.$('input').blur() && openSearchModal();
+ this.blur() && openSearchModal();
}
},
}}
diff --git a/framework/core/js/src/common/components/AutocompleteDropdown.tsx b/framework/core/js/src/common/components/AutocompleteDropdown.tsx
index 88f01c90f5..beb1aeca27 100644
--- a/framework/core/js/src/common/components/AutocompleteDropdown.tsx
+++ b/framework/core/js/src/common/components/AutocompleteDropdown.tsx
@@ -84,35 +84,35 @@ export default abstract class AutocompleteDropdown<
// Highlight the item that is currently selected.
this.setIndex(this.getCurrentNumericIndex());
- this.$('.Dropdown-suggestions')
- .on('mousedown', (e) => e.preventDefault())
- // Whenever the mouse is hovered over a search result, highlight it.
- .on('mouseenter', '> li:not(.Dropdown-header)', function () {
- component.setIndex(component.selectableItems().index(this));
- });
+ const suggestions = this.element.querySelector('.Dropdown-suggestions')! as HTMLDivElement;
+ suggestions.addEventListener('mousedown', (e) => e.preventDefault());
+ // Whenever the mouse is hovered over a search result, highlight it.
+ suggestions.addEventListener('mouseenter', (e) => {
+ const el = e.target as HTMLElement;
+ if (el.parentElement != suggestions || el.tagName != 'LI' || el.classList.contains('Dropdown-header')) return;
+ component.setIndex(component.selectableItems().indexOf(el as HTMLLIElement));
+ });
- const $input = this.inputElement();
+ const input = this.inputElement();
this.navigator = new KeyboardNavigatable();
this.navigator
.onUp(() => this.setIndex(this.getCurrentNumericIndex() - 1, true))
.onDown(() => this.setIndex(this.getCurrentNumericIndex() + 1, true))
.onSelect(this.selectSuggestion.bind(this), true)
- .bindTo($input);
-
- $input
- .on('focus', function () {
- component.hasFocus = true;
- m.redraw();
-
- $(this)
- .one('mouseup', (e) => e.preventDefault())
- .trigger('select');
- })
- .on('blur', function () {
- component.hasFocus = false;
- m.redraw();
- });
+ .bindTo(input);
+
+ input.addEventListener('focus', function () {
+ component.hasFocus = true;
+ m.redraw();
+
+ this.addEventListener('mouseup', (e) => e.preventDefault(), { once: true });
+ this.select();
+ });
+ input.addEventListener('blur', () => {
+ component.hasFocus = false;
+ m.redraw();
+ });
this.updateMaxHeightHandler = this.updateMaxHeight.bind(this);
window.addEventListener('resize', this.updateMaxHeightHandler);
@@ -126,16 +126,16 @@ export default abstract class AutocompleteDropdown<
}
}
- selectableItems(): JQuery {
- return this.$('.Dropdown-suggestions > li:not(.Dropdown-header)');
+ selectableItems(): HTMLLIElement[] {
+ return Array.from(this.element.querySelectorAll('.Dropdown-suggestions > li:not(.Dropdown-header)')) as HTMLLIElement[];
}
- inputElement(): JQuery {
- return this.$('input') as JQuery;
+ inputElement(): HTMLInputElement {
+ return this.element.querySelector('input') as HTMLInputElement;
}
selectSuggestion() {
- this.getItem(this.index).find('button')[0].click();
+ this.getItem(this.index).querySelector('button')!.click();
}
/**
@@ -143,21 +143,21 @@ export default abstract class AutocompleteDropdown<
* Returns zero if not found.
*/
getCurrentNumericIndex(): number {
- return Math.max(0, this.selectableItems().index(this.getItem(this.index)));
+ return Math.max(0, this.selectableItems().indexOf(this.getItem(this.index)));
}
/**
* Get the in the search results with the given index (numeric or named).
*/
- getItem(index: number): JQuery {
- const $items = this.selectableItems();
- let $item = $items.filter(`[data-index="${index}"]`);
+ getItem(index: number): HTMLLIElement {
+ const items = this.selectableItems();
+ const filtered = items.filter((v) => v.getAttribute('data-index') == index.toString());
- if (!$item.length) {
- $item = $items.eq(index);
+ if (!filtered.length) {
+ return items[index];
}
- return $item;
+ return filtered[0];
}
/**
@@ -165,36 +165,44 @@ export default abstract class AutocompleteDropdown<
* index.
*/
setIndex(index: number, scrollToItem: boolean = false) {
- const $items = this.selectableItems();
- const $dropdown = $items.parent();
+ const items = this.selectableItems();
let fixedIndex = index;
if (index < 0) {
- fixedIndex = $items.length - 1;
- } else if (index >= $items.length) {
+ fixedIndex = items.length - 1;
+ } else if (index >= items.length) {
fixedIndex = 0;
}
- const $item = $items.removeClass('active').eq(fixedIndex).addClass('active');
+ items.forEach((el) => el.classList.remove('active'));
+ const item = items[fixedIndex];
+ const dropdown = item.parentElement!;
+ item.classList.add('active');
- this.index = parseInt($item.attr('data-index') as string) || fixedIndex;
+ this.index = parseInt(item.getAttribute('data-index') as string) || fixedIndex;
if (scrollToItem) {
- const dropdownScroll = $dropdown.scrollTop()!;
- const dropdownTop = $dropdown.offset()!.top;
- const dropdownBottom = dropdownTop + $dropdown.outerHeight()!;
- const itemTop = $item.offset()!.top;
- const itemBottom = itemTop + $item.outerHeight()!;
+ const documentScrollTop = document.documentElement.scrollTop;
+ const dropdownScroll = dropdown.scrollTop!;
+ const dropdownRect = dropdown.getBoundingClientRect();
+ const dropdownTop = dropdownRect.top + documentScrollTop;
+ const dropdownBottom = dropdownTop + dropdownRect.height;
+ const itemRect = item.getBoundingClientRect();
+ const itemTop = itemRect.top + documentScrollTop;
+ const itemBottom = itemTop + itemRect.height;
let scrollTop;
if (itemTop < dropdownTop) {
- scrollTop = dropdownScroll - dropdownTop + itemTop - parseInt($dropdown.css('padding-top'), 10);
+ scrollTop = dropdownScroll - dropdownTop + itemTop - parseInt(getComputedStyle(dropdown).paddingTop, 10);
} else if (itemBottom > dropdownBottom) {
- scrollTop = dropdownScroll - dropdownBottom + itemBottom + parseInt($dropdown.css('padding-bottom'), 10);
+ scrollTop = dropdownScroll - dropdownBottom + itemBottom + parseInt(getComputedStyle(dropdown).paddingBottom, 10);
}
if (typeof scrollTop !== 'undefined') {
- $dropdown.stop(true).animate({ scrollTop }, 100);
+ dropdown.scrollTo({
+ top: scrollTop,
+ behavior: 'smooth',
+ });
}
}
}
diff --git a/framework/core/js/src/common/components/ConfirmDocumentUnload.js b/framework/core/js/src/common/components/ConfirmDocumentUnload.js
index 063fb79747..95cc1ea92c 100644
--- a/framework/core/js/src/common/components/ConfirmDocumentUnload.js
+++ b/framework/core/js/src/common/components/ConfirmDocumentUnload.js
@@ -19,13 +19,13 @@ export default class ConfirmDocumentUnload extends Component {
super.oncreate(vnode);
this.boundHandler = this.handler.bind(this);
- $(window).on('beforeunload', this.boundHandler);
+ window.addEventListener('beforeunload', this.boundHandler);
}
onremove(vnode) {
super.onremove(vnode);
- $(window).off('beforeunload', this.boundHandler);
+ window.removeEventListener('beforeunload', this.boundHandler);
}
view(vnode) {
diff --git a/framework/core/js/src/common/components/Dropdown.tsx b/framework/core/js/src/common/components/Dropdown.tsx
index 268159cc4b..c014e74054 100644
--- a/framework/core/js/src/common/components/Dropdown.tsx
+++ b/framework/core/js/src/common/components/Dropdown.tsx
@@ -40,6 +40,8 @@ export interface IDropdownAttrs extends ComponentAttrs {
export default class Dropdown extends Component {
protected showing = false;
+ protected backdropElement: HTMLDivElement | null = null;
+
static initAttrs(attrs: IDropdownAttrs) {
attrs.className ||= '';
attrs.buttonClassName ||= '';
@@ -67,7 +69,7 @@ export default class Dropdown {
+ this.element.addEventListener('shown.bs.dropdown', () => {
const { lazyDraw, onshow } = this.attrs;
this.showing = true;
@@ -88,31 +90,16 @@ export default class Dropdown windowSrollTop + windowHeight);
-
- if (($menu.offset()?.top || 0) < 0) {
- $menu.removeClass('Dropdown-menu--top');
- }
-
- const left = $menu.offset()?.left ?? 0;
- const width = $menu.width() ?? 0;
- const windowScrollLeft = $(window).scrollLeft() ?? 0;
- const windowWidth = $(window).width() ?? 0;
-
- $menu.toggleClass('Dropdown-menu--right', isRight || left + width > windowScrollLeft + windowWidth);
+ // Mithril doesn't really redraw this component sometimes (e.g. Discussion list)
+ // Bootstrap 5 has removed the open class toggle and the backdrop
+ // these need to be added manually.
+ this.element.classList.add('open');
+ this.backdropElement = document.createElement('div');
+ this.backdropElement.classList.add('dropdown-backdrop');
+ this.element.append(this.backdropElement);
});
- this.$().on('hidden.bs.dropdown', () => {
+ this.element.addEventListener('hidden.bs.dropdown', () => {
this.showing = false;
if (this.attrs.onhide) {
@@ -120,6 +107,8 @@ export default class Dropdown
diff --git a/framework/core/js/src/common/components/EditUserModal.tsx b/framework/core/js/src/common/components/EditUserModal.tsx
index b5bed5e2a0..36707005ac 100644
--- a/framework/core/js/src/common/components/EditUserModal.tsx
+++ b/framework/core/js/src/common/components/EditUserModal.tsx
@@ -109,7 +109,7 @@ export default class EditUserModal {
);
requestAnimationFrame(() => {
- this.activeDialogElement?.classList.add('in');
+ this.activeDialogElement?.classList.add('show');
});
}
diff --git a/framework/core/js/src/common/components/Page.tsx b/framework/core/js/src/common/components/Page.tsx
index b47183d439..e491d350f1 100644
--- a/framework/core/js/src/common/components/Page.tsx
+++ b/framework/core/js/src/common/components/Page.tsx
@@ -43,11 +43,11 @@ export default abstract class Page {
this.query(value);
- this.inputScroll(this.inputElement()[0]?.scrollLeft ?? 0);
+ this.inputScroll(this.inputElement()?.scrollLeft ?? 0);
}}
inputAttrs={{ className: 'SearchModal-input' }}
renderInput={(attrs: any) => (
@@ -218,7 +218,7 @@ export default class SearchModal;
+ const input = this.inputElement();
+ if (!input) return;
this.navigator = new KeyboardNavigatable();
this.navigator
@@ -288,12 +289,14 @@ export default class SearchModal this.setIndex(this.getCurrentNumericIndex() + 1, true))
.onSelect(this.selectResult.bind(this), true)
.onCancel(this.clear.bind(this))
- .bindTo($input);
+ .bindTo(input);
// Handle input key events on the search input, triggering results to load.
- $input.on('input focus', function () {
- search(this.value.toLowerCase());
- });
+ ['input', 'focus'].forEach((ev) =>
+ input.addEventListener(ev as 'input' | 'focus', function () {
+ search(this.value.toLowerCase());
+ })
+ );
}
onremove(vnode: Mithril.VnodeDOM) {
@@ -430,8 +433,8 @@ export default class SearchModal {
- return this.$('.SearchModal-input') as JQuery;
+ inputElement(): HTMLInputElement | null {
+ return this.element?.querySelector('.SearchModal-input');
}
defaultActiveSource(): string | null {
diff --git a/framework/core/js/src/common/components/TextEditor.js b/framework/core/js/src/common/components/TextEditor.js
index 2360df1338..0139fde908 100644
--- a/framework/core/js/src/common/components/TextEditor.js
+++ b/framework/core/js/src/common/components/TextEditor.js
@@ -77,7 +77,7 @@ export default class TextEditor extends Component {
}
onbuild() {
- this.attrs.composer.editor = this.buildEditor(this.$('.TextEditor-editorContainer')[0]);
+ this.attrs.composer.editor = this.buildEditor(this.element.querySelector('.TextEditor-editorContainer'));
}
onupdate(vnode) {
diff --git a/framework/core/js/src/common/components/Tooltip.tsx b/framework/core/js/src/common/components/Tooltip.tsx
index 7c0791a057..49213b33e4 100644
--- a/framework/core/js/src/common/components/Tooltip.tsx
+++ b/framework/core/js/src/common/components/Tooltip.tsx
@@ -2,6 +2,7 @@ import Component from '../Component';
import type Mithril from 'mithril';
import classList from '../utils/classList';
import extractText from '../utils/extractText';
+import BootstrapTooltip from 'bootstrap/js/dist/tooltip';
export interface TooltipAttrs extends Mithril.CommonAttributes {
/**
@@ -97,6 +98,8 @@ export interface TooltipAttrs extends Mithril.CommonAttributes
*/
export default class Tooltip extends Component {
+ private tooltip: BootstrapTooltip | null = null;
+
private firstChild: Mithril.Vnode | null = null;
private childDomNode: HTMLElement | null = null;
@@ -186,11 +189,7 @@ export default class Tooltip extends Component {
private recreateTooltip() {
if (this.shouldRecreateTooltip && this.childDomNode !== null) {
- $(this.childDomNode).tooltip(
- 'destroy',
- // @ts-expect-error We don't want this arg to be part of the public API. It only exists to prevent deprecation warnings when using `$.tooltip` in this component.
- 'DANGEROUS_tooltip_jquery_fn_deprecation_exempt'
- );
+ this.tooltip?.dispose();
this.createTooltip();
this.shouldRecreateTooltip = false;
}
@@ -205,17 +204,9 @@ export default class Tooltip extends Component {
if (this.childDomNode === null) return;
if (this.attrs.tooltipVisible === true) {
- $(this.childDomNode).tooltip(
- 'show',
- // @ts-expect-error We don't want this arg to be part of the public API. It only exists to prevent deprecation warnings when using `$.tooltip` in this component.
- 'DANGEROUS_tooltip_jquery_fn_deprecation_exempt'
- );
+ this.tooltip?.show();
} else if (this.attrs.tooltipVisible === false) {
- $(this.childDomNode).tooltip(
- 'hide',
- // @ts-expect-error We don't want this arg to be part of the public API. It only exists to prevent deprecation warnings when using `$.tooltip` in this component.
- 'DANGEROUS_tooltip_jquery_fn_deprecation_exempt'
- );
+ this.tooltip?.hide();
}
}
@@ -239,17 +230,13 @@ export default class Tooltip extends Component {
this.childDomNode.setAttribute('title', realText);
this.childDomNode.setAttribute('aria-label', realText);
- // https://getbootstrap.com/docs/3.3/javascript/#tooltips-options
- $(this.childDomNode).tooltip(
- {
- html,
- delay,
- placement: position,
- trigger,
- },
- // @ts-expect-error We don't want this arg to be part of the public API. It only exists to prevent deprecation warnings when using `$.tooltip` in this component.
- 'DANGEROUS_tooltip_jquery_fn_deprecation_exempt'
- );
+ // https://getbootstrap.com/docs/5.0/components/tooltips/#options
+ this.tooltip = new BootstrapTooltip(this.childDomNode, {
+ html,
+ delay: delay || 0,
+ placement: position,
+ trigger: trigger as any,
+ });
}
private getRealText(): string {
diff --git a/framework/core/js/src/common/components/UploadImageButton.tsx b/framework/core/js/src/common/components/UploadImageButton.tsx
index 4a55db0769..26708b9946 100644
--- a/framework/core/js/src/common/components/UploadImageButton.tsx
+++ b/framework/core/js/src/common/components/UploadImageButton.tsx
@@ -52,29 +52,28 @@ export default class UploadImageButton');
-
- $input
- .appendTo('body')
- .hide()
- .trigger('click')
- .on('change', (e) => {
- const body = new FormData();
- // @ts-ignore
- body.append(this.attrs.name, $(e.target)[0].files[0]);
-
- this.loading = true;
- m.redraw();
-
- app
- .request({
- method: 'POST',
- url: this.resourceUrl(),
- serialize: (raw) => raw,
- body,
- })
- .then(this.success.bind(this), this.failure.bind(this));
- });
+ const input = document.createElement('input');
+ input.type = 'file';
+ input.style.display = 'none';
+ document.body.append(input);
+ input.click();
+ input.addEventListener('change', (e) => {
+ const body = new FormData();
+ // @ts-ignore
+ body.append(this.attrs.name, $(e.target)[0].files[0]);
+
+ this.loading = true;
+ m.redraw();
+
+ app
+ .request({
+ method: 'POST',
+ url: this.resourceUrl(),
+ serialize: (raw) => raw,
+ body,
+ })
+ .then(this.success.bind(this), this.failure.bind(this));
+ });
}
remove() {
diff --git a/framework/core/js/src/common/helpers/highlight.tsx b/framework/core/js/src/common/helpers/highlight.tsx
index bebfbdf5f4..41d9a0b061 100644
--- a/framework/core/js/src/common/helpers/highlight.tsx
+++ b/framework/core/js/src/common/helpers/highlight.tsx
@@ -32,7 +32,9 @@ export default function highlight(string: string, phrase?: string | RegExp, leng
// Convert the string into HTML entities, then highlight all matches with
// tags. Then we will return the result as a trusted HTML string.
if (!safe) {
- highlighted = $('').text(highlighted).html();
+ const el = document.createElement('div');
+ el.textContent = highlighted;
+ highlighted = el.innerHTML;
}
if (phrase) highlighted = highlighted.replace(regexp, '$&');
diff --git a/framework/core/js/src/common/index.ts b/framework/core/js/src/common/index.ts
index 0b7deb7004..d5ff24a54d 100644
--- a/framework/core/js/src/common/index.ts
+++ b/framework/core/js/src/common/index.ts
@@ -3,15 +3,17 @@ import 'expose-loader?exposes=$,jQuery!jquery';
import 'expose-loader?exposes=m!mithril';
import 'expose-loader?exposes=dayjs!dayjs';
-import 'bootstrap/js/affix';
-import 'bootstrap/js/dropdown';
-import 'bootstrap/js/tooltip';
-import 'bootstrap/js/transition';
-import 'jquery.hotkeys/jquery.hotkeys';
-
+import Dropdown from 'bootstrap/js/dist/dropdown';
import relativeTime from 'dayjs/plugin/relativeTime';
import localizedFormat from 'dayjs/plugin/localizedFormat';
+import popperMobileModifier from './utils/popperMobileModifier';
+
+Dropdown.Default.popperConfig = {
+ strategy: 'fixed',
+ modifiers: [popperMobileModifier],
+};
+
dayjs.extend(relativeTime);
dayjs.extend(localizedFormat);
@@ -26,19 +28,3 @@ import app from './app';
export { app };
import './utils/arrayFlatPolyfill';
-
-const tooltipGen = $.fn.tooltip;
-
-// Remove in a future version of Flarum.
-// @ts-ignore
-$.fn.tooltip = function (options, caller) {
- // Show a warning when `$.tooltip` is used outside of the Tooltip component.
- // This functionality is deprecated and should not be used.
- if (!['DANGEROUS_tooltip_jquery_fn_deprecation_exempt'].includes(caller)) {
- console.warn(
- "Calling `$.tooltip` is now deprecated. Please use the `` component exposed by flarum/core instead. `$.tooltip` may be removed in a future version of Flarum.\n\nIf this component doesn't meet your requirements, please open an issue: https://github.com/flarum/core/issues/new?assignees=davwheat&labels=type/bug,needs-verification&template=bug-report.md&title=Tooltip%20component%20unsuitable%20for%20use%20case"
- );
- }
-
- tooltipGen.bind(this)(options);
-};
diff --git a/framework/core/js/src/common/utils/Drawer.js b/framework/core/js/src/common/utils/Drawer.js
index dbf66945bc..f3d0a3f79c 100644
--- a/framework/core/js/src/common/utils/Drawer.js
+++ b/framework/core/js/src/common/utils/Drawer.js
@@ -87,14 +87,15 @@ export default class Drawer {
if (!this.isOpen()) return;
- const $drawer = $('#drawer');
+ const drawer = document.getElementById('drawer');
// Used to prevent `visibility: hidden` from breaking the exit animation
- $drawer.css('visibility', 'visible').one('transitionend', () => $drawer.css('visibility', ''));
+ drawer.style.visibility = 'visible';
+ drawer.addEventListener('transitionend', () => (drawer.style.visibility = ''), { once: true });
this.appElement.classList.remove('drawerOpen');
- this.$backdrop?.remove?.();
+ this.backdrop.remove();
}
/**
@@ -105,10 +106,13 @@ export default class Drawer {
this.drawerAvailableMediaQuery.addListener(this.resizeHandler);
- this.$backdrop = $('').addClass('drawer-backdrop fade').appendTo('body').on('click', this.hide.bind(this));
+ this.backdrop = document.createElement('div');
+ this.backdrop.classList.add('drawer-backdrop', 'fade');
+ this.backdrop.addEventListener('click', this.hide.bind(this));
+ document.body.append(this.backdrop);
requestAnimationFrame(() => {
- this.$backdrop.addClass('in');
+ this.backdrop.classList.add('show');
this.focusTrap.activate();
});
diff --git a/framework/core/js/src/common/utils/GambitsAutocomplete.tsx b/framework/core/js/src/common/utils/GambitsAutocomplete.tsx
index 6379d612d2..78e99212ed 100644
--- a/framework/core/js/src/common/utils/GambitsAutocomplete.tsx
+++ b/framework/core/js/src/common/utils/GambitsAutocomplete.tsx
@@ -9,7 +9,7 @@ export default class GambitsAutocomplete {
constructor(
public resource: string,
- public jqueryInput: () => JQuery,
+ public inputElement: () => HTMLInputElement | null,
public onchange: (value: string) => void,
public afterSuggest: (value: string) => void
) {}
@@ -55,7 +55,7 @@ export default class GambitsAutocomplete {
const autocompleteReader = new AutocompleteReader(null);
- const cursorPosition = this.jqueryInput().prop('selectionStart') || query.length;
+ const cursorPosition = this.inputElement()?.selectionStart || query.length;
const lastChunk = query.slice(0, cursorPosition);
const autocomplete = autocompleteReader.check(lastChunk, cursorPosition, /\S+$/);
@@ -157,15 +157,15 @@ export default class GambitsAutocomplete {
}
suggest(text: string, fromTyped: string, start: number) {
- const $input = this.jqueryInput();
+ const input = this.inputElement();
const query = this.query;
const replaced = query.slice(0, start) + text + query.slice(start + fromTyped.length);
this.onchange(replaced);
- $input[0].focus();
+ input?.focus();
setTimeout(() => {
- $input[0].setSelectionRange(start + text.length, start + text.length);
+ input?.setSelectionRange(start + text.length, start + text.length);
m.redraw();
}, 50);
diff --git a/framework/core/js/src/common/utils/KeyboardNavigatable.ts b/framework/core/js/src/common/utils/KeyboardNavigatable.ts
index 4966a836c2..611c29bacb 100644
--- a/framework/core/js/src/common/utils/KeyboardNavigatable.ts
+++ b/framework/core/js/src/common/utils/KeyboardNavigatable.ts
@@ -151,11 +151,11 @@ export default class KeyboardNavigatable {
}
/**
- * Set up the navigation key bindings on the given jQuery element.
+ * Set up the navigation key bindings on the given element.
*/
- bindTo($element: JQuery) {
+ bindTo(element: HTMLElement) {
// Handle navigation key events on the navigatable element.
- $element[0].addEventListener('keydown', this.navigate.bind(this));
+ element.addEventListener('keydown', this.navigate.bind(this));
}
/**
diff --git a/framework/core/js/src/common/utils/anchorScroll.js b/framework/core/js/src/common/utils/anchorScroll.js
index 8a18ea48df..35056cd6d5 100644
--- a/framework/core/js/src/common/utils/anchorScroll.js
+++ b/framework/core/js/src/common/utils/anchorScroll.js
@@ -12,10 +12,11 @@
* @param {() => void} callback The callback to run that will change page content.
*/
export default function anchorScroll(element, callback) {
- const $window = $(window);
- const relativeScroll = $(element).offset().top - $window.scrollTop();
+ if (typeof element === 'string') element = document.querySelector(element);
+ if (!element) return;
+ const relativeScroll = element.getBoundingClientRect().top;
callback();
- $window.scrollTop($(element).offset().top - relativeScroll);
+ window.scrollTo({ top: element.getBoundingClientRect().top + document.documentElement.scrollTop - relativeScroll });
}
diff --git a/framework/core/js/src/common/utils/heightWithMargin.ts b/framework/core/js/src/common/utils/heightWithMargin.ts
new file mode 100644
index 0000000000..c983b37014
--- /dev/null
+++ b/framework/core/js/src/common/utils/heightWithMargin.ts
@@ -0,0 +1,4 @@
+export default function heightWithMargin(element: HTMLElement): number {
+ const style = getComputedStyle(element);
+ return element.getBoundingClientRect().height + parseInt(style.marginBottom, 10) + parseInt(style.marginTop, 10);
+}
diff --git a/framework/core/js/src/common/utils/liveHumanTimes.ts b/framework/core/js/src/common/utils/liveHumanTimes.ts
index bcb0d53754..f054c39cd7 100644
--- a/framework/core/js/src/common/utils/liveHumanTimes.ts
+++ b/framework/core/js/src/common/utils/liveHumanTimes.ts
@@ -1,11 +1,10 @@
import humanTime from './humanTime';
function updateHumanTimes() {
- $('[data-humantime]').each(function () {
- const $this = $(this);
- const ago = humanTime($this.attr('datetime'));
+ document.querySelectorAll('[data-humantime]').forEach(function (el) {
+ const ago = humanTime(el.getAttribute('datetime'));
- $this.html(ago);
+ el.textContent = ago;
});
}
diff --git a/framework/core/js/src/common/utils/popperMobileModifier.ts b/framework/core/js/src/common/utils/popperMobileModifier.ts
new file mode 100644
index 0000000000..7b12893720
--- /dev/null
+++ b/framework/core/js/src/common/utils/popperMobileModifier.ts
@@ -0,0 +1,20 @@
+import { type Modifier } from '@popperjs/core';
+
+export default {
+ name: 'responsiveMobile',
+ enabled: true,
+ phase: 'beforeWrite',
+ fn({ state }) {
+ const screen = getComputedStyle(state.elements.popper).getPropertyValue('--flarum-screen');
+ if (screen == 'phone') {
+ state.styles.popper = {
+ margin: null as unknown as string,
+ position: null as unknown as string,
+ left: null as unknown as string,
+ top: null as unknown as string,
+ bottom: null as unknown as string,
+ transform: null as unknown as string,
+ };
+ }
+ },
+} satisfies Modifier;
diff --git a/framework/core/js/src/common/utils/scrollEnd.ts b/framework/core/js/src/common/utils/scrollEnd.ts
new file mode 100644
index 0000000000..60c1e52ca2
--- /dev/null
+++ b/framework/core/js/src/common/utils/scrollEnd.ts
@@ -0,0 +1,24 @@
+/**
+ * A utility function that waits for the scroll to end.
+ * Due to lack of support from some browsers, this is a workaround for `scrollend` event.
+ *
+ * @see https://developer.mozilla.org/en-US/docs/Web/API/Element/scrollTo#behavior
+ * @see https://caniuse.com/mdn-api_element_scrollend_event
+ * @param container The container element to wait scroll end on.
+ * @returns A promise that resolves when the scroll is ended.
+ */
+export default function scrollEnd(container: HTMLElement): Promise {
+ let lastTop = container.scrollTop;
+
+ return new Promise((resolve) => {
+ const animFrame = () => {
+ if (lastTop === container.scrollTop) {
+ resolve();
+ } else {
+ requestAnimationFrame(animFrame);
+ lastTop = container.scrollTop;
+ }
+ };
+ requestAnimationFrame(animFrame);
+ });
+}
diff --git a/framework/core/js/src/forum/ForumApplication.tsx b/framework/core/js/src/forum/ForumApplication.tsx
index 4bd195ca15..b9d2e1a9eb 100644
--- a/framework/core/js/src/forum/ForumApplication.tsx
+++ b/framework/core/js/src/forum/ForumApplication.tsx
@@ -137,9 +137,11 @@ export default class ForumApplication extends Application {
});
if (isSafariMobile()) {
- $(() => {
- $('.App').addClass('mobile-safari');
- });
+ const callback = () => {
+ document.querySelector('.App')?.classList.add('mobile-safari');
+ };
+ document.addEventListener('DOMContentLoaded', callback);
+ if (document.readyState != 'loading') callback();
}
}
diff --git a/framework/core/js/src/forum/components/AbstractPost.tsx b/framework/core/js/src/forum/components/AbstractPost.tsx
index 0b261cea00..aac96e735d 100644
--- a/framework/core/js/src/forum/components/AbstractPost.tsx
+++ b/framework/core/js/src/forum/components/AbstractPost.tsx
@@ -66,8 +66,6 @@ export default abstract class AbstractPost this.$('.Post-controls').addClass('open')}
- onhide={() => this.$('.Post-controls').removeClass('open')}
accessibleToggleLabel={app.translator.trans('core.forum.post_controls.toggle_dropdown_accessible_label')}
>
{controls}
@@ -92,10 +90,9 @@ export default abstract class AbstractPost) {
super.onupdate(vnode);
- const $actions = this.$('.Post-actions');
- const $controls = this.$('.Post-controls');
-
- $actions.toggleClass('openWithin', $controls.hasClass('open'));
+ this.element
+ .querySelector('.Post-actions')
+ ?.classList.toggle('openWithin', this.element.querySelector('.Post-controls')?.classList.contains('open'));
}
elementAttrs(): Record {
diff --git a/framework/core/js/src/forum/components/AffixedSidebar.js b/framework/core/js/src/forum/components/AffixedSidebar.js
index 3cba6e88ef..8632edc0d2 100644
--- a/framework/core/js/src/forum/components/AffixedSidebar.js
+++ b/framework/core/js/src/forum/components/AffixedSidebar.js
@@ -1,15 +1,14 @@
import Component from '../../common/Component';
+import heightWithMargin from '../../common/utils/heightWithMargin';
/**
- * The `AffixedSidebar` component uses Bootstrap's "affix" plugin to keep a
+ * The `AffixedSidebar` component uses sticky position to keep a
* sidebar navigation at the top of the viewport when scrolling.
*
* ### Children
*
* The component must wrap an element that itself wraps an element, which
* will be "affixed".
- *
- * @see https://getbootstrap.com/docs/3.4/javascript/#affix
*/
export default class AffixedSidebar extends Component {
view(vnode) {
@@ -19,35 +18,36 @@ export default class AffixedSidebar extends Component {
oncreate(vnode) {
super.oncreate(vnode);
- // Register the affix plugin to execute on every window resize (and trigger)
+ // Register the affix to execute on every window resize (and trigger)
this.boundOnresize = this.onresize.bind(this);
- $(window).on('resize', this.boundOnresize).resize();
+ window.addEventListener('resize', this.boundOnresize);
+ window.dispatchEvent(new Event('resize'));
}
onremove(vnode) {
super.onremove(vnode);
- $(window).off('resize', this.boundOnresize);
+ window.removeEventListener('resize', this.boundOnresize);
}
onresize() {
- const $sidebar = this.$();
- const $header = $('#header');
- const $footer = $('#footer');
- const $affixElement = $sidebar.find('> ul');
-
- $(window).off('.affix');
- $affixElement.removeClass('affix affix-top affix-bottom').removeData('bs.affix');
+ const header = document.getElementById('header');
+ const affixElement = this.element.querySelector(':scope > ul');
+ const pageSidebar = this.element.closest('.Page-sidebar');
// Don't affix the sidebar if it is taller than the viewport (otherwise
// there would be no way to scroll through its content).
- if ($sidebar.outerHeight(true) > $(window).height() - $header.outerHeight(true)) return;
-
- $affixElement.affix({
- offset: {
- top: () => $sidebar.offset().top - $header.outerHeight(true) - parseInt($sidebar.css('margin-top'), 10),
- bottom: () => (this.bottom = $footer.outerHeight(true)),
- },
- });
+ const enabled = heightWithMargin(this.element) <= window.innerHeight - heightWithMargin(header);
+ affixElement.classList.toggle('affix', enabled);
+ if (enabled) {
+ const top = heightWithMargin(header) + parseInt(getComputedStyle(pageSidebar ?? this.element).marginTop, 10);
+ affixElement.style.position = 'sticky';
+ affixElement.style.top = top + 'px';
+ this.element.style.display = 'initial'; // Workaround for sticky not working
+ } else {
+ affixElement.style.position = '';
+ affixElement.style.top = '';
+ this.element.style.display = '';
+ }
}
}
diff --git a/framework/core/js/src/forum/components/AvatarEditor.js b/framework/core/js/src/forum/components/AvatarEditor.js
index fd1da14226..c40ae7968e 100644
--- a/framework/core/js/src/forum/components/AvatarEditor.js
+++ b/framework/core/js/src/forum/components/AvatarEditor.js
@@ -149,15 +149,15 @@ export default class AvatarEditor extends Component {
// Create a hidden HTML input element and click on it so the user can select
// an avatar file. Once they have, we will upload it via the API.
- const $input = $('');
-
- $input
- .appendTo('body')
- .hide()
- .click()
- .on('input', (e) => {
- this.upload($(e.target)[0].files[0]);
- });
+ const input = document.createElement('input');
+ input.type = 'file';
+ input.accept = '.jpg, .jpeg, .png, .bmp, .gif';
+ input.style.display = 'none';
+ input.click();
+ input.addEventListener('input', (e) => {
+ this.upload(e.target.files[0]);
+ });
+ document.body.append(input);
}
/**
diff --git a/framework/core/js/src/forum/components/CommentPost.js b/framework/core/js/src/forum/components/CommentPost.js
index ab17245794..beac77418d 100644
--- a/framework/core/js/src/forum/components/CommentPost.js
+++ b/framework/core/js/src/forum/components/CommentPost.js
@@ -82,11 +82,11 @@ export default class CommentPost extends Post {
// all of the