Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Remove jQuery & Bootstrap #4111

Draft
wants to merge 17 commits into
base: 2.x
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions extensions/embed/js/src/forum/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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);
}
});
Expand Down
28 changes: 15 additions & 13 deletions extensions/emoji/js/src/forum/addComposerAutocomplete.js
Original file line number Diff line number Diff line change
Expand Up @@ -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('<div class="ComposerBody-emojiWrapper"></div>');
const editor = this.element.querySelector('.TextEditor-editor');
editor.outerHTML = `<div class="ComposerBody-emojiWrapper">${editor.outerHTML}</div>`;

this.navigator = new KeyboardNavigatable();
this.navigator
Expand All @@ -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($('<div class="ComposerBody-emojiDropdownContainer"></div>'));
editor.outerHTML = editor.outerHTML + '<div class="ComposerBody-emojiDropdownContainer"></div>';
});

extend('flarum/common/components/TextEditor', 'buildEditorParams', function (params) {
Expand Down Expand Up @@ -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);
}
Expand All @@ -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;
}
});
Expand Down
49 changes: 28 additions & 21 deletions extensions/emoji/js/src/forum/fragments/AutocompleteDropdown.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand All @@ -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',
});
}
}
}
Expand Down
6 changes: 3 additions & 3 deletions framework/core/js/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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",
Expand Down
8 changes: 8 additions & 0 deletions framework/core/js/src/@types/global.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,14 @@ declare type VnodeElementTag<Attrs = Record<string, unknown>, 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');

Expand Down
17 changes: 7 additions & 10 deletions framework/core/js/src/admin/components/AdminNav.js
Original file line number Diff line number Diff line change
Expand Up @@ -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'
});
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -199,7 +199,7 @@ export default class CreateUserModal<CustomAttrs extends ICreateUserModalAttrs =
}

onready() {
this.$('[name=username]').trigger('select');
(this.element.querySelector('[name="username"]') as HTMLInputElement).select();
}

onsubmit(e: SubmitEvent | null = null) {
Expand Down
32 changes: 18 additions & 14 deletions framework/core/js/src/admin/components/UserListPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -303,13 +303,13 @@ export default class UserListPage extends AdminPage {
name: app.translator.trans('core.admin.users.grid.columns.email.title'),
content: (user: User) => {
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
Expand All @@ -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);
Expand Down
1 change: 1 addition & 0 deletions framework/core/js/src/common/Component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,7 @@ export default abstract class Component<Attrs extends ComponentAttrs = Component
* @param [selector] a jQuery-compatible selector string
* @returns the jQuery object for the DOM node
* @final
* @deprecated We are moving away from jQuery.
*/
$(selector?: string): JQuery {
const $element = $(this.element) as JQuery<HTMLElement>;
Expand Down
1 change: 1 addition & 0 deletions framework/core/js/src/common/Fragment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<HTMLElement>;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -86,12 +86,17 @@ export default abstract class AbstractGlobalSearch<T extends SearchAttrs = Searc
this.searchState = this.attrs.state;
}

blur() {
this.element.querySelector('input')?.blur();
return true;
}

view() {
// Hide the search view if no sources were loaded
if (this.sourceItems().isEmpty()) return <div></div>;

const openSearchModal = () => {
this.$('input').blur() &&
this.blur() &&
app.modal.show(() => import('../../common/components/SearchModal'), { searchState: this.searchState, sources: this.sourceItems().toArray() });
};

Expand All @@ -101,7 +106,7 @@ export default abstract class AbstractGlobalSearch<T extends SearchAttrs = Searc
className="Search"
aria-label={this.attrs.a11yRoleLabel}
onclick={() => {
this.$('input').blur();
this.blur();
setTimeout(() => openSearchModal(), 150);
}}
>
Expand All @@ -124,7 +129,7 @@ export default abstract class AbstractGlobalSearch<T extends SearchAttrs = Searc
onkeydown: (e: KeyboardEvent) => {
if (e.key === 'Enter') {
e.preventDefault();
this.$('input').blur() && openSearchModal();
this.blur() && openSearchModal();
}
},
}}
Expand Down
Loading
Loading