diff --git a/app/controllers/categories_controller.rb b/app/controllers/categories_controller.rb
index 55fa8acf1bc3..c5f5b4f6c4fd 100644
--- a/app/controllers/categories_controller.rb
+++ b/app/controllers/categories_controller.rb
@@ -40,29 +40,15 @@ def new
@category = @project.categories.build
end
- def create # rubocop:disable Metrics/AbcSize
+ def create
@category = @project.categories.build
@category.attributes = permitted_params.category
if @category.save
- respond_to do |format|
- format.html do
- flash[:notice] = I18n.t(:notice_successful_create)
- redirect_to project_settings_categories_path(@project)
- end
- format.js do
- render locals: { project: @project, category: @category }
- end
- end
+ flash[:notice] = I18n.t(:notice_successful_create)
+ redirect_to project_settings_categories_path(@project)
else
- respond_to do |format|
- format.html do
- render action: :new, status: :unprocessable_entity
- end
- format.js do
- render(:update) { |page| page.alert(@category.errors.full_messages.join('\n')) }
- end
- end
+ render action: :new, status: :unprocessable_entity
end
end
diff --git a/app/helpers/removed_js_helpers_helper.rb b/app/helpers/removed_js_helpers_helper.rb
index 03d66b371780..65d2ac235039 100644
--- a/app/helpers/removed_js_helpers_helper.rb
+++ b/app/helpers/removed_js_helpers_helper.rb
@@ -45,7 +45,11 @@ def link_to_function(content, function, html_options = {})
# Execute the callback on click
def csp_onclick(callback_str, selector, prevent_default: true)
content_for(:additional_js_dom_ready) do
- "jQuery('#{selector}').click(function() { #{callback_str}; #{prevent_default ? 'return false;' : ''} });\n".html_safe
+ raw <<~JS # rubocop:disable Rails/OutputSafety
+ document.querySelector('#{j(selector)}')?.addEventListener('click', function(event) {
+ #{callback_str&.delete_suffix(';')};#{"\n event.preventDefault();" if prevent_default}
+ });
+ JS
end
end
end
diff --git a/app/views/categories/create.js.erb b/app/views/categories/create.js.erb
deleted file mode 100644
index e21944ffa7a7..000000000000
--- a/app/views/categories/create.js.erb
+++ /dev/null
@@ -1,30 +0,0 @@
-<%#-- copyright
-OpenProject is an open source project management software.
-Copyright (C) the OpenProject GmbH
-
-This program is free software; you can redistribute it and/or
-modify it under the terms of the GNU General Public License version 3.
-
-OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
-Copyright (C) 2006-2013 Jean-Philippe Lang
-Copyright (C) 2010-2013 the ChiliProject Team
-
-This program is free software; you can redistribute it and/or
-modify it under the terms of the GNU General Public License
-as published by the Free Software Foundation; either version 2
-of the License, or (at your option) any later version.
-
-This program is distributed in the hope that it will be useful,
-but WITHOUT ANY WARRANTY; without even the implied warranty of
-MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-GNU General Public License for more details.
-
-You should have received a copy of the GNU General Public License
-along with this program; if not, write to the Free Software
-Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-See COPYRIGHT and LICENSE files for more details.
-
-++#%>
-
-jQuery('#work_package_category_id').empty().append("<%= escape_javascript(options_from_collection_for_select(project.categories, "id", "name", category.id)) %>");
diff --git a/app/views/forums/show.html.erb b/app/views/forums/show.html.erb
index f696241a1ccc..869f1fbfe501 100644
--- a/app/views/forums/show.html.erb
+++ b/app/views/forums/show.html.erb
@@ -27,28 +27,6 @@ See COPYRIGHT and LICENSE files for more details.
++#%>
-
- <% if authorize_for('messages', 'new') %>
-
<%= link_to h(@forum.name), project_forum_path(@project, @forum) %> » <%= t(:label_message_new) %>
- <%= labelled_tabular_form_for Message.new(project: @project),
- url: forum_topics_path(@forum),
- html: {
- multipart: true,
- id: "message-form",
- class: "form",
- data: { turbo: false }
- } do |f| %>
- <%= render partial: "messages/form", locals: { f: f } %>
-
-
- <%= styled_button_tag t(:button_create), class: "-primary -with-icon icon-checkmark" %>
- <%= link_to t(:button_cancel), "", class: "cancel-add-message-button button -with-icon icon-cancel" %>
- <% csp_onclick('jQuery("#add-message").hide();', ".cancel-add-message-button") %>
- <% end %>
-
- <% end %>
-
-
<%=
render(Primer::OpenProject::PageHeader.new) do |header|
header.with_title { @forum.name }
@@ -70,7 +48,6 @@ See COPYRIGHT and LICENSE files for more details.
leading_icon: :plus,
label: t(:label_message_new),
tag: :a,
- class: "add-message-button",
href: url_for({ controller: "/messages", action: "new", forum_id: @forum })) do
t(:label_message)
end
diff --git a/app/views/layouts/base.html.erb b/app/views/layouts/base.html.erb
index d9070e03b8f9..7d5623131af5 100644
--- a/app/views/layouts/base.html.erb
+++ b/app/views/layouts/base.html.erb
@@ -200,14 +200,10 @@ See COPYRIGHT and LICENSE files for more details.
}
wrapper.style.display = '';
-<% end %>
-<%= nonced_javascript_tag do %>
- (function($) {
- // Wrapper for page-specific JS from deprecated inline functions
- // no longer available with CSP.
- <%= content_for :additional_js_dom_ready %>
- }(jQuery));
+ // Wrapper for page-specific JS from deprecated inline functions
+ // no longer available with CSP.
+ <%= content_for :additional_js_dom_ready %>
<% end %>
<%= call_hook :view_layouts_base_body_bottom %>
diff --git a/app/views/news/show.html.erb b/app/views/news/show.html.erb
index a2dcaccc55b7..c4c1ffe0eda2 100644
--- a/app/views/news/show.html.erb
+++ b/app/views/news/show.html.erb
@@ -47,7 +47,6 @@ See COPYRIGHT and LICENSE files for more details.
aria: { label: I18n.t(:button_edit) },
title: I18n.t(:button_edit)
) do |button|
- csp_onclick('jQuery("#edit-news").show()', ".edit-news-button")
button.with_leading_visual_icon(icon: :pencil)
t(:button_edit)
end
@@ -72,16 +71,6 @@ See COPYRIGHT and LICENSE files for more details.
end
%>
-<% if authorize_for('news', 'edit') %>
-
- <%= labelled_tabular_form_for @news, html: { id: "news-form" } do |f| %>
- <%= render partial: "form", locals: { f: f } %>
- <%= styled_button_tag t(:button_save), class: "-primary -with-icon icon-checkmark" %>
- <%= link_to_function t(:button_cancel), 'jQuery("#edit-news").hide()',
- class: "button -with-icon icon-cancel" %>
- <% end %>
-
-<% end %>
<% if @news.summary.present? %>
<%= format_text(@news, :summary) %>
diff --git a/app/views/projects/settings/repository/subversion/_existing.html.erb b/app/views/projects/settings/repository/subversion/_existing.html.erb
index 0807381f6c4d..e451890a9d10 100644
--- a/app/views/projects/settings/repository/subversion/_existing.html.erb
+++ b/app/views/projects/settings/repository/subversion/_existing.html.erb
@@ -67,10 +67,16 @@ See COPYRIGHT and LICENSE files for more details.
) %>
<%=
content_for(:additional_js_dom_ready) do
- "jQuery('#repository-password-placeholder')
- .change(function() { this.name = 'repository[password]'; })
- .focus(function() { this.value = ''; this.name = 'repository[password]'; });
- ".html_safe
+ raw <<~JS
+ const placeholder = document.getElementById('repository-password-placeholder');
+ placeholder?.addEventListener('input', function(event) {
+ placeholder.name = 'repository[password]';
+ });
+ placeholder?.addEventListener('focus', function(event) {
+ placeholder.value = '';
+ placeholder.name = 'repository[password]';
+ });
+ JS
end
%>
diff --git a/app/views/repositories/_revisions.html.erb b/app/views/repositories/_revisions.html.erb
index c823bfe20a2e..e234cf338a20 100644
--- a/app/views/repositories/_revisions.html.erb
+++ b/app/views/repositories/_revisions.html.erb
@@ -121,8 +121,9 @@ See COPYRIGHT and LICENSE files for more details.
(line_num == 1),
id: "cb-#{line_num}" %>
<% csp_onclick(
- "jQuery('#cbto-#{line_num + 1}').attr('checked', true);",
- "cb-#{line_num}"
+ "document.getElementById('cbto-#{line_num + 1}').checked = true;",
+ "#cb-#{line_num}",
+ prevent_default: false
) %>
<% end %>
@@ -134,8 +135,9 @@ See COPYRIGHT and LICENSE files for more details.
(line_num == 2),
id: "cbto-#{line_num}" %>
<% csp_onclick(
- "if (jQuery('#cb-#{line_num}').attr('checked')) {jQuery('#cb-#{line_num - 1}').attr('checked', true)}",
- "cbto-#{line_num}"
+ "if (document.getElementById('cb-#{line_num}')?.checked) {document.getElementById('cb-#{line_num - 1}').checked = true;}",
+ "#cbto-#{line_num}",
+ prevent_default: false
) %>
<% end %>
diff --git a/app/views/repositories/diff.html.erb b/app/views/repositories/diff.html.erb
index 3733ddaafa31..e598558ae2a7 100644
--- a/app/views/repositories/diff.html.erb
+++ b/app/views/repositories/diff.html.erb
@@ -50,18 +50,16 @@ See COPYRIGHT and LICENSE files for more details.
end
%>
- <%= form_tag({repo_path: to_path_param(@path)}, method: :get) do %>
- <%= hidden_field_tag('rev', params[:rev]) if params[:rev] %>
- <%= hidden_field_tag('rev_to', params[:rev_to]) if params[:rev_to] %>
- <%= styled_select_tag 'type', options_for_select([[t(:label_diff_inline), "inline"], [t(:label_diff_side_by_side), "sbs"]], @diff_type), id: "repository-diff-type-select" %>
+ <%= form_tag({ repo_path: to_path_param(@path) }, method: :get, data: { controller: "auto-submit" }) do %>
+ <%= hidden_field_tag("rev", params[:rev]) if params[:rev] %>
+ <%= hidden_field_tag("rev_to", params[:rev_to]) if params[:rev_to] %>
+ <%= styled_select_tag "type",
+ options_for_select(
+ [[t(:label_diff_inline), "inline"],
+ [t(:label_diff_side_by_side), "sbs"]], @diff_type
+ ),
+ data: { action: "auto-submit#submit" } %>
<% end %>
- <%=
- content_for(:additional_js_dom_ready) do
- "jQuery('#repository-diff-type-select').change(function() {
- if (this.value != '') { this.form.submit() }
- });".html_safe
- end
- %>
<% cache(@cache_key) do -%>
<%= render partial: 'common/diff', locals: { diff: @diff, diff_type: @diff_type } %>
diff --git a/app/views/versions/create.js.erb b/app/views/versions/create.js.erb
deleted file mode 100644
index 3107e98ff5e2..000000000000
--- a/app/views/versions/create.js.erb
+++ /dev/null
@@ -1,30 +0,0 @@
-<%#-- copyright
-OpenProject is an open source project management software.
-Copyright (C) the OpenProject GmbH
-
-This program is free software; you can redistribute it and/or
-modify it under the terms of the GNU General Public License version 3.
-
-OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
-Copyright (C) 2006-2013 Jean-Philippe Lang
-Copyright (C) 2010-2013 the ChiliProject Team
-
-This program is free software; you can redistribute it and/or
-modify it under the terms of the GNU General Public License
-as published by the Free Software Foundation; either version 2
-of the License, or (at your option) any later version.
-
-This program is distributed in the hope that it will be useful,
-but WITHOUT ANY WARRANTY; without even the implied warranty of
-MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-GNU General Public License for more details.
-
-You should have received a copy of the GNU General Public License
-along with this program; if not, write to the Free Software
-Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-See COPYRIGHT and LICENSE files for more details.
-
-++#%>
-
-jQuery('#work_package_version_id').empty().append("<%= escape_javascript(version_options_for_select(versions, version)) %>");
diff --git a/app/views/wiki/destroy.html.erb b/app/views/wiki/destroy.html.erb
index 9c0b2e2e0764..96d2ed9189d5 100644
--- a/app/views/wiki/destroy.html.erb
+++ b/app/views/wiki/destroy.html.erb
@@ -62,7 +62,7 @@ See COPYRIGHT and LICENSE files for more details.
<%= styled_select_tag "reassign_to_id",
options_for_select(wiki_page_options_for_select(@reassignable_to)),
{ container_class: "-wide" } %>
- <% csp_onclick("jQuery('#todo_reassign').attr('checked', true)", "#reassign_to_id") %>
+ <% csp_onclick("document.getElementById('todo_reassign').checked = true", "#reassign_to_id") %>
<% end %>
diff --git a/app/views/wiki/edit_parent_page.html.erb b/app/views/wiki/edit_parent_page.html.erb
index 0604858712e7..134339e8acd0 100644
--- a/app/views/wiki/edit_parent_page.html.erb
+++ b/app/views/wiki/edit_parent_page.html.erb
@@ -39,18 +39,19 @@ See COPYRIGHT and LICENSE files for more details.
<%= error_messages_for "page" %>
<%= labelled_tabular_form_for @page, url: { id: @page, action: "update_parent_page" } do |f| %>
+ <% options_for_select = wiki_page_options_for_select(@parent_pages) %>
<%= f.select :parent_id,
- wiki_page_options_for_select(@parent_pages),
- { label: WikiPage.human_attribute_name(:parent_title), include_blank: false, container_class: "-wide" } %>
+ options_for_select,
+ { label: WikiPage.human_attribute_name(:parent_title), include_blank: false, container_class: "-wide" },
+ {
+ size: options_for_select.size + 1,
+ style: "height:unset",
+ data: {
+ controller: "select-autosize",
+ select_autosize_size_limit_value: 8
+ }
+ } %>
-
- <%= nonced_javascript_tag do -%>
- jQuery(function() {
- var parent_select = jQuery('#wiki_page_parent_id');
- parent_select.attr('size', parent_select.children().length);
- }
- ));
- <% end -%>
<%= submit_tag t(:button_save), class: "button -primary" %>
<% end %>
diff --git a/app/views/work_packages/bulk/destroy.html.erb b/app/views/work_packages/bulk/destroy.html.erb
index d9b4db1a9aa1..687a05a218e5 100644
--- a/app/views/work_packages/bulk/destroy.html.erb
+++ b/app/views/work_packages/bulk/destroy.html.erb
@@ -28,7 +28,16 @@ See COPYRIGHT and LICENSE files for more details.
++#%>
<%= error_messages_for work_packages.first %>
-<%= styled_form_tag work_packages_bulk_path, method: :delete, class: "form danger-zone" do %>
+<%=
+ styled_form_tag(
+ work_packages_bulk_path,
+ method: :delete,
+ class: "form danger-zone",
+ data: {
+ controller: "show-when-value-selected"
+ }
+ ) do
+%>
<%= back_url_hidden_field_tag unless back_url_is_wp_show? %>
<% work_packages.each do |work_package| %>
<%= hidden_field_tag "ids[]", work_package.id %>
@@ -49,13 +58,21 @@ See COPYRIGHT and LICENSE files for more details.
@@ -63,11 +80,11 @@ See COPYRIGHT and LICENSE files for more details.
@@ -75,7 +92,15 @@ See COPYRIGHT and LICENSE files for more details.
diff --git a/frontend/eslint.config.mjs b/frontend/eslint.config.mjs
index 3b10106130d7..6516882b6e42 100644
--- a/frontend/eslint.config.mjs
+++ b/frontend/eslint.config.mjs
@@ -58,8 +58,7 @@ export default defineConfig([
},
],
- // Sometimes we need to shush the TypeScript compiler
- 'no-unused-vars': ['error', { varsIgnorePattern: '^_', argsIgnorePattern: '^_' }],
+ 'no-unused-vars': 'off',
'@typescript-eslint/no-unused-vars': ['error', { varsIgnorePattern: '^_', argsIgnorePattern: '^_' }],
// Allow short circuit evaluations
@@ -154,13 +153,6 @@ export default defineConfig([
'@angular-eslint/template/prefer-control-flow': 'error'
}
},
- {
- files: ['**/*.d.ts'],
- rules: {
- 'no-unused-vars': 'off',
- '@typescript-eslint/no-unused-vars': 'off',
- },
- },
{
files: ['**/*.spec.ts'],
plugins: { jasmine },
diff --git a/frontend/package-lock.json b/frontend/package-lock.json
index 53039641370f..e73fdadec87b 100644
--- a/frontend/package-lock.json
+++ b/frontend/package-lock.json
@@ -47,6 +47,7 @@
"@hotwired/stimulus": "^3.2.2",
"@hotwired/turbo": "^8.0.20",
"@hotwired/turbo-rails": "^8.0.20",
+ "@knowledgecode/delegate": "^0.8.5",
"@kolkov/ngx-gallery": "^2.0.1",
"@mantine/core": "^8.3.6",
"@mantine/hooks": "^8.3.5",
@@ -82,6 +83,7 @@
"dom-autoscroller": "^2.2.8",
"dom-plane": "^1.0.2",
"dragula": "^3.7.3",
+ "es6-slide-up-down": "^1.0.0",
"flatpickr": "^4.6.13",
"glob": "^11.0.3",
"hammerjs": "^2.0.8",
@@ -4970,6 +4972,12 @@
"tslib": "2"
}
},
+ "node_modules/@knowledgecode/delegate": {
+ "version": "0.8.5",
+ "resolved": "https://registry.npmjs.org/@knowledgecode/delegate/-/delegate-0.8.5.tgz",
+ "integrity": "sha512-Wuv1m5t170SWW6R60zgOKg14s230buKfrd8tjpmeG/lKBFcYjQSvZoOZh34isygkvJkBHY2ozdg2E3rzJy3Ehg==",
+ "license": "MIT"
+ },
"node_modules/@kolkov/ngx-gallery": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/@kolkov/ngx-gallery/-/ngx-gallery-2.0.1.tgz",
@@ -11628,6 +11636,12 @@
"node": ">=0.12"
}
},
+ "node_modules/es6-slide-up-down": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/es6-slide-up-down/-/es6-slide-up-down-1.0.0.tgz",
+ "integrity": "sha512-s86t2F+GjPRxiSodC59JRZZaP07Ht03EDU2RDrRCXYk9o3CfTCNjtwqrpoV1BjbCnzXcBoDGj1aZhzZkHzMpWQ==",
+ "license": "MIT"
+ },
"node_modules/es6-symbol": {
"version": "3.1.4",
"resolved": "https://registry.npmjs.org/es6-symbol/-/es6-symbol-3.1.4.tgz",
@@ -26596,6 +26610,11 @@
"integrity": "sha512-sw/RMbehRhN68WRtcKCpQOPfnH6lLP4GJfqzi3iYej8tnzpZUDr6UkZYJjcjjC0FWEJOJbyM3PTIwxucUmDG2A==",
"dev": true
},
+ "@knowledgecode/delegate": {
+ "version": "0.8.5",
+ "resolved": "https://registry.npmjs.org/@knowledgecode/delegate/-/delegate-0.8.5.tgz",
+ "integrity": "sha512-Wuv1m5t170SWW6R60zgOKg14s230buKfrd8tjpmeG/lKBFcYjQSvZoOZh34isygkvJkBHY2ozdg2E3rzJy3Ehg=="
+ },
"@kolkov/ngx-gallery": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/@kolkov/ngx-gallery/-/ngx-gallery-2.0.1.tgz",
@@ -31088,6 +31107,11 @@
}
}
},
+ "es6-slide-up-down": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/es6-slide-up-down/-/es6-slide-up-down-1.0.0.tgz",
+ "integrity": "sha512-s86t2F+GjPRxiSodC59JRZZaP07Ht03EDU2RDrRCXYk9o3CfTCNjtwqrpoV1BjbCnzXcBoDGj1aZhzZkHzMpWQ=="
+ },
"es6-symbol": {
"version": "3.1.4",
"resolved": "https://registry.npmjs.org/es6-symbol/-/es6-symbol-3.1.4.tgz",
diff --git a/frontend/package.json b/frontend/package.json
index 7225dea73ba7..fe68ebb6a4c4 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -102,6 +102,7 @@
"@hotwired/stimulus": "^3.2.2",
"@hotwired/turbo": "^8.0.20",
"@hotwired/turbo-rails": "^8.0.20",
+ "@knowledgecode/delegate": "^0.8.5",
"@kolkov/ngx-gallery": "^2.0.1",
"@mantine/core": "^8.3.6",
"@mantine/hooks": "^8.3.5",
@@ -137,6 +138,7 @@
"dom-autoscroller": "^2.2.8",
"dom-plane": "^1.0.2",
"dragula": "^3.7.3",
+ "es6-slide-up-down": "^1.0.0",
"flatpickr": "^4.6.13",
"glob": "^11.0.3",
"hammerjs": "^2.0.8",
diff --git a/frontend/src/app/core/current-project/current-project.service.spec.ts b/frontend/src/app/core/current-project/current-project.service.spec.ts
index 3f3654ad4ff0..59ada2ac2818 100644
--- a/frontend/src/app/core/current-project/current-project.service.spec.ts
+++ b/frontend/src/app/core/current-project/current-project.service.spec.ts
@@ -30,7 +30,7 @@ import { PathHelperService } from 'core-app/core/path-helper/path-helper.service
import { CurrentProjectService } from './current-project.service';
describe('currentProject service', () => {
- let element:JQuery;
+ let element:HTMLMetaElement;
let currentProject:CurrentProjectService;
const apiV3Stub:any = {
@@ -55,12 +55,12 @@ describe('currentProject service', () => {
describe('with a meta value present', () => {
beforeEach(() => {
- const html = `
-
- `;
-
- element = jQuery(html);
- jQuery(document.body).append(element);
+ element = document.createElement('meta');
+ element.setAttribute('name', 'current_project');
+ element.dataset.projectName = 'Foo 1234';
+ element.dataset.projectId = '1';
+ element.dataset.projectIdentifier = 'foobar';
+ document.head.appendChild(element);
currentProject.detect();
});
diff --git a/frontend/src/app/core/current-project/current-project.service.ts b/frontend/src/app/core/current-project/current-project.service.ts
index 454509c1136d..da1425eda32a 100644
--- a/frontend/src/app/core/current-project/current-project.service.ts
+++ b/frontend/src/app/core/current-project/current-project.service.ts
@@ -29,6 +29,7 @@
import { Injectable } from '@angular/core';
import { PathHelperService } from 'core-app/core/path-helper/path-helper.service';
import { ApiV3Service } from 'core-app/core/apiv3/api-v3.service';
+import { getMetaElement } from '../setup/globals/global-helpers';
@Injectable({ providedIn: 'root' })
export class CurrentProjectService {
@@ -79,11 +80,11 @@ export class CurrentProjectService {
* Detect the current project from its meta tag.
*/
public detect() {
- const element:HTMLMetaElement|null = document.querySelector('meta[name=current_project]');
+ const element = getMetaElement('current_project');
if (element) {
- this.currentId = element.dataset.projectId as string;
- this.currentName = element.dataset.projectName as string;
- this.currentIdentifier = element.dataset.projectIdentifier as string;
+ this.currentId = element.dataset.projectId!;
+ this.currentName = element.dataset.projectName!;
+ this.currentIdentifier = element.dataset.projectIdentifier!;
} else {
this.currentId = null;
this.currentName = null;
diff --git a/frontend/src/app/core/current-user/current-user.module.ts b/frontend/src/app/core/current-user/current-user.module.ts
index 125ee43f4795..892368c38147 100644
--- a/frontend/src/app/core/current-user/current-user.module.ts
+++ b/frontend/src/app/core/current-user/current-user.module.ts
@@ -4,13 +4,13 @@ import { CurrentUserService } from './current-user.service';
import { CurrentUserStore } from './current-user.store';
import { CurrentUserQuery } from './current-user.query';
import { firstValueFrom } from 'rxjs';
+import { getMetaValue } from '../setup/globals/global-helpers';
function loadUserMetadata(currentUserService:CurrentUserService) {
- const userMeta = document.querySelector('meta[name=current_user]');
currentUserService.setUser({
- id: userMeta?.dataset.id || null,
- name: userMeta?.dataset.name || null,
- loggedIn: userMeta?.dataset.loggedIn === 'true',
+ id: getMetaValue('current_user', 'id', null),
+ name: getMetaValue('current_user', 'name', null),
+ loggedIn: getMetaValue('current_user', 'loggedIn') === 'true'
});
}
diff --git a/frontend/src/app/core/errors/appsignal/appsignal-reporter.ts b/frontend/src/app/core/errors/appsignal/appsignal-reporter.ts
index af1b29655916..30be48c84c1b 100644
--- a/frontend/src/app/core/errors/appsignal/appsignal-reporter.ts
+++ b/frontend/src/app/core/errors/appsignal/appsignal-reporter.ts
@@ -30,6 +30,7 @@ import { debugLog } from 'core-app/shared/helpers/debug_output';
import { ErrorReporterBase, ErrorTags, MessageSeverity } from 'core-app/core/errors/error-reporter-base';
import type { Appsignal } from './appsignal-dependency';
import { Span } from '@appsignal/javascript';
+import { getMetaElement } from 'core-app/core/setup/globals/global-helpers';
export class AppsignalReporter extends ErrorReporterBase {
private client:Appsignal;
@@ -50,7 +51,7 @@ export class AppsignalReporter extends ErrorReporterBase {
constructor() {
super();
- const element = document.querySelector('meta[name=openproject_appsignal]') as HTMLElement;
+ const element = getMetaElement('openproject_appsignal')!;
this.loadAppsignal(element);
}
diff --git a/frontend/src/app/core/errors/configure-reporter.ts b/frontend/src/app/core/errors/configure-reporter.ts
index c19892c5e798..c1a884f4b0be 100644
--- a/frontend/src/app/core/errors/configure-reporter.ts
+++ b/frontend/src/app/core/errors/configure-reporter.ts
@@ -1,9 +1,10 @@
import { ErrorReporterBase } from 'core-app/core/errors/error-reporter-base';
import { AppsignalReporter } from 'core-app/core/errors/appsignal/appsignal-reporter';
import { LocalReporter } from 'core-app/core/errors/local/local-reporter';
+import { getMetaElement } from '../setup/globals/global-helpers';
export function configureErrorReporter():ErrorReporterBase {
- const appsignalElement = document.querySelector('meta[name=openproject_appsignal]') as HTMLElement;
+ const appsignalElement = getMetaElement('openproject_appsignal');
if (appsignalElement !== null) {
return new AppsignalReporter();
}
diff --git a/frontend/src/app/core/global_search/input/global-search-input.component.ts b/frontend/src/app/core/global_search/input/global-search-input.component.ts
index f32f7ce0ba32..64ed483ea07a 100644
--- a/frontend/src/app/core/global_search/input/global-search-input.component.ts
+++ b/frontend/src/app/core/global_search/input/global-search-input.component.ts
@@ -165,7 +165,7 @@ export class GlobalSearchInputComponent implements AfterViewInit, OnDestroy {
// detect if click is outside or inside the element
@HostListener('click', ['$event'])
- public handleClick(event:JQuery.TriggeredEvent):void {
+ public handleClick(event:MouseEvent):void {
event.preventDefault();
// handle click on search button
@@ -173,7 +173,7 @@ export class GlobalSearchInputComponent implements AfterViewInit, OnDestroy {
if (this.deviceService.isTablet) {
this.toggleMobileSearch();
// open ng-select menu on default
- jQuery('.ng-input input').focus();
+ document.querySelector('.ng-input input')?.focus();
// only for mobile and not for all devices!
// See https://github.com/opf/openproject/commit/a2eb0cd6025f2ecaca00f4ed81c4eb8e9399bd86
event.stopPropagation();
diff --git a/frontend/src/app/core/html/op-title.service.ts b/frontend/src/app/core/html/op-title.service.ts
index bd53e664f800..fc6a500ecb2a 100644
--- a/frontend/src/app/core/html/op-title.service.ts
+++ b/frontend/src/app/core/html/op-title.service.ts
@@ -1,5 +1,6 @@
import { Title } from '@angular/platform-browser';
import { Injectable } from '@angular/core';
+import { getMetaContent } from '../setup/globals/global-helpers';
const titlePartsSeparator = ' | ';
@@ -13,8 +14,7 @@ export class OpTitleService {
}
public get base():string {
- const appTitle = document.querySelector('meta[name=app_title]') as HTMLMetaElement;
- return appTitle.content;
+ return getMetaContent('app_title');
}
public get titleParts():string[] {
diff --git a/frontend/src/app/core/i18n/i18n.service.ts b/frontend/src/app/core/i18n/i18n.service.ts
index cf1ca76ad9b8..76cdf244165b 100644
--- a/frontend/src/app/core/i18n/i18n.service.ts
+++ b/frontend/src/app/core/i18n/i18n.service.ts
@@ -2,6 +2,7 @@ import { Injectable } from '@angular/core';
import { NgSelectConfig } from '@ng-select/ng-select';
import { I18n } from 'i18n-js';
import { FormatNumberOptions, TranslateOptions } from 'i18n-js/src/typing';
+import { getMetaValue } from '../setup/globals/global-helpers';
@Injectable({ providedIn: 'root' })
export class I18nService {
@@ -11,8 +12,7 @@ export class I18nService {
constructor(
private config:NgSelectConfig,
) {
- const meta = document.querySelector('meta[name=openproject_initializer]');
- this.instanceLocale = meta?.dataset.instancelocale || 'en';
+ this.instanceLocale = getMetaValue('openproject_initializer', 'instanceLocale', 'en');
this.config.addTagText = this.t('js.autocomplete_ng_select.add_tag');
this.config.clearAllText = this.t('js.autocomplete_ng_select.clear_all');
diff --git a/frontend/src/app/core/loading-indicator/loading-indicator.service.ts b/frontend/src/app/core/loading-indicator/loading-indicator.service.ts
index c2665bb00399..cff8b3965043 100644
--- a/frontend/src/app/core/loading-indicator/loading-indicator.service.ts
+++ b/frontend/src/app/core/loading-indicator/loading-indicator.service.ts
@@ -70,7 +70,7 @@ export class LoadingIndicator {
`;
- constructor(public indicator:JQuery) {
+ constructor(public indicator:HTMLElement) {
}
public set promise(promise:Promise) {
@@ -87,7 +87,7 @@ export class LoadingIndicator {
public start() {
// If we're currently having an active indicator, remove that one
this.stop();
- this.indicator.prepend(this.indicatorTemplate);
+ this.indicator.insertAdjacentHTML('afterbegin', this.indicatorTemplate);
}
public delayedStop(time = 25) {
@@ -95,7 +95,7 @@ export class LoadingIndicator {
}
public stop() {
- this.indicator.find('.loading-indicator--background').remove();
+ this.indicator.querySelector('.loading-indicator--background')?.remove();
}
}
@@ -121,7 +121,7 @@ export class LoadingIndicatorService {
}
// Return an indicator by name or element
- public indicator(indicator:string|JQuery):LoadingIndicator {
+ public indicator(indicator:string|HTMLElement):LoadingIndicator {
if (typeof indicator === 'string') {
indicator = this.getIndicatorAt(indicator);
}
@@ -129,7 +129,7 @@ export class LoadingIndicatorService {
return new LoadingIndicator(indicator);
}
- private getIndicatorAt(name:string):JQuery {
- return jQuery(indicatorLocationSelector).filter(`[data-indicator-name="${name}"]`);
+ private getIndicatorAt(name:string):HTMLElement {
+ return document.querySelector(`${indicatorLocationSelector}[data-indicator-name="${name}"]`)!;
}
}
diff --git a/frontend/src/app/core/main-menu/main-menu-toggle.service.ts b/frontend/src/app/core/main-menu/main-menu-toggle.service.ts
index 494e0f901140..c7c087d81096 100644
--- a/frontend/src/app/core/main-menu/main-menu-toggle.service.ts
+++ b/frontend/src/app/core/main-menu/main-menu-toggle.service.ts
@@ -32,6 +32,7 @@ import { I18nService } from 'core-app/core/i18n/i18n.service';
import { CurrentProjectService } from 'core-app/core/current-project/current-project.service';
import { DeviceService } from 'core-app/core/browser/device.service';
import { InjectField } from 'core-app/shared/helpers/angular/inject-field.decorator';
+import { queryVisible } from 'core-app/shared/helpers/dom-helpers';
@Injectable({ providedIn: 'root' })
export class MainMenuToggleService {
@@ -51,7 +52,7 @@ export class MainMenuToggleService {
private htmlNode = document.getElementsByTagName('html')[0];
- private mainMenu = jQuery('#main-menu')[0]; // main menu, containing sidebar and resizer
+ private mainMenu = document.querySelector('#main-menu')!; // main menu, containing sidebar and resizer
// Notes all changes of the menu size (currently needed in wp-resizer.component.ts)
private changeData = new BehaviorSubject(undefined);
@@ -110,7 +111,7 @@ export class MainMenuToggleService {
}
}
- public toggleNavigation(event?:JQuery.TriggeredEvent|Event):void {
+ public toggleNavigation(event?:Event):void {
if (event) {
event.stopPropagation();
event.preventDefault();
@@ -132,14 +133,15 @@ export class MainMenuToggleService {
// This needs to be called after AngularJS has rendered the menu, which happens some when after(!) we leave this
// method here. So we need to set the focus after a timeout.
setTimeout(() => {
- jQuery('#main-menu [class*="-menu-item"]:visible').first().focus();
+ const firstVisibleMenuItem = queryVisible('[class*="-menu-item"]', this.mainMenu)[0];
+ firstVisibleMenuItem?.focus();
}, 500);
}
public closeMenu():void {
this.setWidth(0);
this.changeData.next(0);
- jQuery('.searchable-menu--search-input').blur();
+ document.querySelectorAll('.searchable-menu--search-input').forEach((input) => input.blur());
}
public openMenu():void {
@@ -187,7 +189,7 @@ export class MainMenuToggleService {
private toggleClassHidden():void {
const isHidden = this.elementWidth < this.elementMinWidth;
- const hideElements = jQuery('.can-hide-navigation');
- hideElements.toggleClass('hidden-navigation', isHidden);
+ const hideElements = document.querySelectorAll('.can-hide-navigation');
+ hideElements.forEach((hideElement) => hideElement.classList.toggle('hidden-navigation', isHidden));
}
}
diff --git a/frontend/src/app/core/routing/openproject.routes.ts b/frontend/src/app/core/routing/openproject.routes.ts
index bed629c8347e..d535682f01e6 100644
--- a/frontend/src/app/core/routing/openproject.routes.ts
+++ b/frontend/src/app/core/routing/openproject.routes.ts
@@ -107,7 +107,7 @@ export function updateMenuItem(menuItemClass:string|undefined, action:'add'|'rem
return;
}
- const menuItem = jQuery(`#main-menu .${menuItemClass}`)[0];
+ const menuItem = document.querySelector(`#main-menu .${menuItemClass}`);
if (!menuItem) {
return;
diff --git a/frontend/src/app/core/setup/globals/components/admin/backup.component.ts b/frontend/src/app/core/setup/globals/components/admin/backup.component.ts
index 215bf6085337..1242bcd5094c 100644
--- a/frontend/src/app/core/setup/globals/components/admin/backup.component.ts
+++ b/frontend/src/app/core/setup/globals/components/admin/backup.component.ts
@@ -101,7 +101,7 @@ export class BackupComponent implements AfterViewInit {
return this.mayIncludeAttachments ? '' : this.text.attachmentsDisabled;
}
- public triggerBackup(event?:JQuery.TriggeredEvent) {
+ public triggerBackup(event?:Event) {
if (event) {
event.stopPropagation();
event.preventDefault();
diff --git a/frontend/src/app/core/setup/globals/global-helpers.ts b/frontend/src/app/core/setup/globals/global-helpers.ts
index 449f6b045a49..3cad73607bcb 100644
--- a/frontend/src/app/core/setup/globals/global-helpers.ts
+++ b/frontend/src/app/core/setup/globals/global-helpers.ts
@@ -47,5 +47,26 @@ export class GlobalHelpers {
}
export function getMetaElement(name:string):HTMLMetaElement|null {
- return document.querySelector(`meta[name=${name}]`);
+ return document.head.querySelector(`meta[name="${CSS.escape(name)}"]`);
+}
+
+export function getMetaContent(name:string):string;
+export function getMetaContent(name:string, defaultValue:T):T;
+export function getMetaContent(
+ name:string,
+ defaultValue?:T
+):string|T {
+ const content = getMetaElement(name)?.content ?? defaultValue ?? '';
+ return content as string|T;
+}
+
+export function getMetaValue(name:string, key:string):string;
+export function getMetaValue(name:string, key:string, defaultValue:T):T;
+export function getMetaValue(
+ name:string,
+ key:string,
+ defaultValue?:T
+):string|T {
+ const value = getMetaElement(name)?.dataset[key] ?? defaultValue ?? '';
+ return value as string|T;
}
diff --git a/frontend/src/app/core/setup/globals/global-listeners.ts b/frontend/src/app/core/setup/globals/global-listeners.ts
index c2a8dca7979e..7b11efdf24c1 100644
--- a/frontend/src/app/core/setup/globals/global-listeners.ts
+++ b/frontend/src/app/core/setup/globals/global-listeners.ts
@@ -35,7 +35,7 @@ import { performAnchorHijacking } from './global-listeners/link-hijacking';
export function initializeGlobalListeners():void {
document
.documentElement
- .addEventListener('click', (evt:MouseEvent) => {
+ .addEventListener('click', (evt) => {
const target = evt.target as HTMLElement;
// Avoid defaulting clicks on elements already removed from DOM
@@ -71,11 +71,9 @@ export function initializeGlobalListeners():void {
});
// Disable global drag & drop handling, which results in the browser loading the image and losing the page
- jQuery(document.documentElement)
- .on('dragover drop', (evt:Event) => {
- evt.preventDefault();
- return false;
- });
+ const disableDragDefaults = (evt:Event) => { evt.preventDefault(); };
+ document.documentElement.addEventListener('dragover', disableDragDefaults);
+ document.documentElement.addEventListener('drop', disableDragDefaults);
// Bootstrap legacy app code
setupServerResponse();
diff --git a/frontend/src/app/core/setup/globals/global-listeners/action-menu.ts b/frontend/src/app/core/setup/globals/global-listeners/action-menu.ts
index b5d67d7f82a4..2c7a010c85aa 100644
--- a/frontend/src/app/core/setup/globals/global-listeners/action-menu.ts
+++ b/frontend/src/app/core/setup/globals/global-listeners/action-menu.ts
@@ -26,7 +26,7 @@
// See COPYRIGHT and LICENSE files for more details.
//++
import { ANIMATION_RATE_MS } from 'core-app/core/top-menu/top-menu.service';
-import ClickEvent = JQuery.ClickEvent;
+import { slideDown, slideUp } from 'es6-slide-up-down';
/*
The action menu is a menu that usually belongs to an OpenProject entity (like an Issue, WikiPage, Meeting, ..).
@@ -43,37 +43,39 @@ import ClickEvent = JQuery.ClickEvent;
The following code is responsible to open and close the "more functions" submenu.
*/
-function closeMenu(event:any) {
- const menu = jQuery(event.data.menu);
+function closeMenu(menu:HTMLElement, event:MouseEvent) {
// do not close the menu, if the user accidentally clicked next to a menu item (but still within the menu)
- if (event.target !== menu.find(' > li.drop-down.open > ul').get(0)) {
- menu.find(' > li.drop-down.open').removeClass('open').find('> ul').slideUp(ANIMATION_RATE_MS);
- // no need to watch for clicks, when the menu is already closed
- jQuery('html').off('click', closeMenu);
+ if (event.target !== menu.querySelector(':scope > li.drop-down.open > ul')) {
+ const li = menu.querySelector(':scope > li.drop-down.open')!;
+ li.classList.remove('open');
+ const ul = li.querySelector(':scope > ul');
+ slideUp(ul!, ANIMATION_RATE_MS);
}
}
-function openMenu(menu:JQuery) {
- const dropDown = menu.find(' > li.drop-down');
+function openMenu(menu:HTMLElement) {
+ const dropDown = menu.querySelector(':scope > li.drop-down')!;
// do not open a menu, which is already open
- if (!dropDown.hasClass('open')) {
- dropDown.find('> ul').slideDown(ANIMATION_RATE_MS, () => {
- dropDown.find('li > a:first').focus();
+ if (!dropDown.classList.contains('open')) {
+ const ul = dropDown.querySelector(':scope > ul')!;
+ slideDown(ul, ANIMATION_RATE_MS);
+ window.requestAnimationFrame(() => {
+ dropDown.querySelector('li > a:first-child')?.focus();
// when clicking on something, which is not the menu, close the menu
- jQuery('html').on('click', { menu: menu.get(0) }, closeMenu);
+ document.addEventListener('click', (evt) => closeMenu(menu, evt), { once: true });
});
- dropDown.addClass('open');
+ dropDown.classList.add('open');
}
}
// open the given submenu when clicking on it
-export function installMenuLogic(menu:JQuery) {
- menu.find(' > li.drop-down').on('click', (event:ClickEvent) => {
+export function installMenuLogic(menu:HTMLElement) {
+ menu.querySelector(':scope > li.drop-down')?.addEventListener('click', (event) => {
openMenu(menu);
// and prevent default action (href) for that element
// but not for the menu items.
- const target = jQuery(event.target);
- if (target.is('.drop-down') || target.closest('li, ul').is('.drop-down')) {
+ const target = event.target;
+ if (target instanceof HTMLElement && (target.matches('.drop-down') || target.closest('li, ul')?.matches('.drop-down'))) {
event.preventDefault();
}
});
diff --git a/frontend/src/app/core/setup/globals/global-listeners/color-preview.ts b/frontend/src/app/core/setup/globals/global-listeners/color-preview.ts
index 5bf4605f378b..05c3e2206752 100644
--- a/frontend/src/app/core/setup/globals/global-listeners/color-preview.ts
+++ b/frontend/src/app/core/setup/globals/global-listeners/color-preview.ts
@@ -33,35 +33,39 @@
* this needs changes
*/
export function makeColorPreviews() {
- jQuery('.color--preview').each(function () {
- const preview = jQuery(this);
- let input:any;
- const target = preview.data('target');
+ document.querySelectorAll('.color--preview').forEach(function (preview) {
+ let input:HTMLInputElement|null = null;
+ const target = preview.dataset.target;
if (target) {
- input = jQuery(target);
+ input = document.querySelector(target);
} else {
- input = preview.next('input');
+ const next = preview.nextElementSibling;
+ if (next && next instanceof HTMLInputElement) {
+ input = next;
+ }
}
- if (input.length === 0) {
+ if (input === null) {
return;
}
- const func = function () {
+ const listener = function () {
let previewColor = '';
- if (input.val() && input.val().length > 0) {
- previewColor = input.val();
- } else if (input.attr('placeholder')
- && input.attr('placeholder').length > 0) {
- previewColor = input.attr('placeholder');
+ if (input.value && input.value.length > 0) {
+ previewColor = input.value;
+ } else if (input.getAttribute('placeholder')
+ && input.getAttribute('placeholder')!.length > 0) {
+ previewColor = input.getAttribute('placeholder')!;
}
- preview.css('background-color', previewColor);
+ preview.style.backgroundColor = previewColor;
};
- input.keyup(func).change(func).focus(func);
- func();
+ input.addEventListener('keyup', listener);
+ input.addEventListener('change', listener);
+ input.addEventListener('focus', listener);
+ listener();
});
}
diff --git a/frontend/src/app/core/setup/globals/global-listeners/danger-zone-validation.ts b/frontend/src/app/core/setup/globals/global-listeners/danger-zone-validation.ts
index becadecda6d9..831235d43fad 100644
--- a/frontend/src/app/core/setup/globals/global-listeners/danger-zone-validation.ts
+++ b/frontend/src/app/core/setup/globals/global-listeners/danger-zone-validation.ts
@@ -30,22 +30,19 @@
// Make the whole danger zone a component the next time this needs changes!
export function dangerZoneValidation() {
- // This will only work iff there is a single danger zone on the page
- const dangerZoneVerification = jQuery('.danger-zone--verification');
- const expectedValue = jQuery('.danger-zone--expected-value');
+ // This will only work if there is a single danger zone on the page
+ const dangerZoneVerification = document.querySelector('.danger-zone--verification')!;
+ const expectedValue = document.querySelector('.danger-zone--expected-value')?.textContent;
// When no expected value is set up, do not disable button
- if (!expectedValue[0]) {
+ if (!expectedValue) {
return;
}
- const expectedText = expectedValue.text();
- dangerZoneVerification.find('input').on('input', () => {
- const actualValue = dangerZoneVerification.find('input').val() as string;
- if (expectedText.toLowerCase() === actualValue.toLowerCase()) {
- dangerZoneVerification.find('button').prop('disabled', false);
- } else {
- dangerZoneVerification.find('button').prop('disabled', true);
- }
+ const input = dangerZoneVerification.querySelector('input')!;
+ const button = dangerZoneVerification.querySelector('button')!;
+ input.addEventListener('input', () => {
+ const actualValue = input.value;
+ button.disabled = expectedValue.toLowerCase() !== actualValue.toLowerCase();
});
}
diff --git a/frontend/src/app/core/setup/globals/global-listeners/settings.ts b/frontend/src/app/core/setup/globals/global-listeners/settings.ts
index e933c2395380..5fc13718760f 100644
--- a/frontend/src/app/core/setup/globals/global-listeners/settings.ts
+++ b/frontend/src/app/core/setup/globals/global-listeners/settings.ts
@@ -1,4 +1,5 @@
import { retrieveCkEditorInstance } from 'core-app/shared/helpers/ckeditor-helpers';
+import { hideElement, showElement, toggleElement } from 'core-app/shared/helpers/dom-helpers';
import invariant from 'tiny-invariant';
/**
@@ -8,20 +9,22 @@ import invariant from 'tiny-invariant';
*/
export function listenToSettingChanges() {
/** Sync SCM vendor select when enabled SCMs are changed */
- jQuery('[name="settings[enabled_scm][]"]').change(function (this:HTMLInputElement) {
- const wasDisabled = !this.checked;
- const vendor = this.value;
- const select = jQuery('#settings_repositories_automatic_managed_vendor');
- const option = select.find(`option[value="${vendor}"]`);
+ const enabledScm = document.querySelector('[name="settings[enabled_scm][]"]');
+ enabledScm?.addEventListener('change', (event) => {
+ const checkbox = event.target as HTMLInputElement;
+ const wasDisabled = !checkbox.checked;
+ const vendor = checkbox.value;
+ const select = document.querySelector('#settings_repositories_automatic_managed_vendor')!;
+ const option = select.querySelector(`option[value="${vendor}"]`);
// Skip non-manageable SCMs
- if (option.length === 0) {
+ if (!option) {
return;
}
- option.prop('disabled', wasDisabled);
- if (wasDisabled && option.prop('selected')) {
- select.val('');
+ option.disabled = wasDisabled;
+ if (wasDisabled && option.selected) {
+ select.value = '';
}
});
@@ -73,43 +76,46 @@ export function listenToSettingChanges() {
/* end Javascript for Settings::TextSettingComponent */
/** Toggle notification settings fields */
- jQuery('#email_delivery_method_switch').on('change', function () {
- const delivery_method = jQuery(this).val();
- jQuery('.email_delivery_method_settings').hide();
- jQuery(`#email_delivery_method_${delivery_method}`).show();
- }).trigger('change');
+ const emailDeliveryMethodSwitch = document.querySelector('#email_delivery_method_switch');
+ emailDeliveryMethodSwitch?.addEventListener('change', (event) => {
+ const delivery_method = (event.target as HTMLSelectElement).value;
+ document
+ .querySelectorAll('.email_delivery_method_settings')
+ .forEach((elem) => hideElement(elem));
+ showElement(document.querySelector(`#email_delivery_method_${delivery_method}`)!);
+ });
+ emailDeliveryMethodSwitch?.dispatchEvent(new Event('change', { bubbles: true }));
- jQuery('#settings_smtp_authentication').on('change', function () {
- const isNone = jQuery(this).val() === 'none';
- jQuery('#settings_smtp_user_name,#settings_smtp_password')
- .closest('.form--field')
- .toggle(!isNone);
+ document.querySelector('#settings_smtp_authentication')?.addEventListener('change', (event) => {
+ const isNone = (event.target as HTMLSelectElement).value === 'none';
+ document
+ .querySelectorAll('#settings_smtp_user_name,#settings_smtp_password')
+ .forEach((field) => toggleElement(field.closest('.form--field')!, !isNone));
});
/** Toggle repository checkout fieldsets required when option is disabled */
- jQuery('.settings-repositories--checkout-toggle').change(function (this:HTMLInputElement) {
- const wasChecked = this.checked;
- const fieldset = jQuery(this).closest('fieldset');
-
- fieldset
- .find('input,select')
- .filter(':not([type=checkbox])')
- .filter(':not([type=hidden])')
- .removeAttr('required') // Rails 4.0 still seems to use attribute
- .prop('required', wasChecked);
+ document.querySelectorAll('.settings-repositories--checkout-toggle').forEach((toggle) => {
+ toggle.addEventListener('change', (event) => {
+ const wasChecked = (event.target as HTMLInputElement).checked;
+ const fieldset = toggle.closest('fieldset')!;
+ fieldset
+ .querySelectorAll('input,select:not([type=checkbox],[type=hidden])')
+ .forEach((field) => { field.required = wasChecked; });
+ });
});
/** Toggle highlighted attributes visibility depending on if the highlighting mode 'inline' was selected */
- jQuery('.settings--highlighting-mode select').change(function () {
- const highlightingMode = jQuery(this).val();
- jQuery('.settings--highlighted-attributes').toggle(highlightingMode === 'inline');
+ document.querySelector('.settings--highlighting-mode select')?.addEventListener('change', (event) => {
+ const highlightingMode = (event.target as HTMLSelectElement).value;
+ document.querySelectorAll('.settings--highlighted-attributes')
+ .forEach((elem) => { toggleElement(elem, highlightingMode === 'inline'); });
});
- jQuery('#tab-content-work_packages form').submit(() => {
- const availableAttributes = jQuery(".settings--highlighted-attributes input[type='checkbox']");
- const selectedAttributes = jQuery(".settings--highlighted-attributes input[type='checkbox']:checked");
+ document.querySelector('#tab-content-work_packages form')?.addEventListener('submit', () => {
+ const availableAttributes = document.querySelectorAll(".settings--highlighted-attributes input[type='checkbox']");
+ const selectedAttributes = document.querySelectorAll(".settings--highlighted-attributes input[type='checkbox']:checked");
if (selectedAttributes.length === availableAttributes.length) {
- availableAttributes.prop('checked', false);
+ availableAttributes.forEach((availableAttribute) => { availableAttribute.checked = false; });
}
});
}
diff --git a/frontend/src/app/core/setup/globals/global-listeners/setup-server-response.ts b/frontend/src/app/core/setup/globals/global-listeners/setup-server-response.ts
index fd0e40cba3fa..bc58d6a27f44 100644
--- a/frontend/src/app/core/setup/globals/global-listeners/setup-server-response.ts
+++ b/frontend/src/app/core/setup/globals/global-listeners/setup-server-response.ts
@@ -1,49 +1,25 @@
// Legacy code ported from app/assets/javascripts/application.js.erb
+import { delegate } from '@knowledgecode/delegate';
+import { showElement } from 'core-app/shared/helpers/dom-helpers';
+import { slideDown, slideUp } from 'es6-slide-up-down';
+
// Do not add stuff here, but ideally remove into components whenever changes are necessary
export function setupServerResponse() {
- jQuery(document).ajaxComplete(activateFlashNotice);
- jQuery(document).ajaxComplete(activateFlashError);
-
- /*
- * 1 - registers a callback which copies the csrf token into the
- * X-CSRF-Token header with each ajax request. Necessary to
- * work with rails applications which have fixed
- * CVE-2011-0447
- * 2 - shows and hides ajax indicator
- */
- jQuery(document).ajaxSend((event, request) => {
- if (jQuery(event.target.activeElement!).closest('[ajax-indicated]').length
- && jQuery('ajax-indicator')) {
- jQuery('#ajax-indicator').show();
- }
-
- const csrf_meta_tag = jQuery('meta[name=csrf-token]');
-
- if (csrf_meta_tag) {
- const header = 'X-CSRF-Token';
- const token = csrf_meta_tag.attr('content');
-
- request.setRequestHeader(header, token!);
- }
-
- request.setRequestHeader('X-Authentication-Scheme', 'Session');
- });
-
- // ajaxStop gets called when ALL Requests finish, so we won't need a counter as in PT
- jQuery(document).ajaxStop(() => {
- if (jQuery('#ajax-indicator')) {
- jQuery('#ajax-indicator').hide();
- }
- addClickEventToAllErrorMessages();
- });
-
// show/hide the files table
- jQuery('.attachments h4').click(function () {
- jQuery(this).toggleClass('closed').next().slideToggle(100);
+ document.querySelectorAll('.attachments h4').forEach((heading) => {
+ heading.addEventListener('click', () => {
+ const closed = heading.classList.toggle('closed');
+ const nextElement = heading.nextElementSibling as HTMLElement;
+ if (closed) {
+ slideUp(nextElement, 100);
+ } else {
+ slideDown(nextElement, 100);
+ }
+ });
});
let resizeTo:any = null;
- jQuery(window).on('resize', () => {
+ window.addEventListener('resize', () => {
// wait 200 milliseconds for no further resize event
// then readjust breadcrumb
@@ -51,23 +27,23 @@ export function setupServerResponse() {
clearTimeout(resizeTo);
}
resizeTo = setTimeout(() => {
- jQuery(window).trigger('resizeEnd');
+ window.dispatchEvent(new CustomEvent('resizeEnd', { bubbles: true }));
}, 200);
});
// Do not close the login window when using it
- jQuery('#nav-login-content').click((event) => {
+ document.querySelector('#nav-login-content')?.addEventListener('click', (event) => {
event.stopPropagation();
});
// Set focus on first error message
- const error_focus = jQuery('a.afocus').first();
- const input_focus = jQuery('.autofocus').first();
- if (error_focus !== undefined) {
+ const error_focus = document.querySelector('a.afocus');
+ const input_focus = document.querySelector('.autofocus');
+ if (error_focus) {
error_focus.focus();
- } else if (input_focus !== undefined) {
+ } else if (input_focus) {
input_focus.focus();
- if (input_focus[0].tagName === 'INPUT') {
+ if (input_focus instanceof HTMLInputElement) {
input_focus.select();
}
}
@@ -75,45 +51,53 @@ export function setupServerResponse() {
addClickEventToAllErrorMessages();
// Click handler for formatting help
- jQuery(document.body).on('click', '.formatting-help-link-button', () => {
- window.open(`${window.appBasePath}/help/wiki_syntax`,
+ delegate(document.body).on('click', '.formatting-help-link-button', (event) => {
+ window.open(
+ `${window.appBasePath}/help/wiki_syntax`,
'',
- 'resizable=yes, location=no, width=600, height=640, menubar=no, status=no, scrollbars=yes');
- return false;
+ 'resizable=yes, location=no, width=600, height=640, menubar=no, status=no, scrollbars=yes'
+ );
+ event.preventDefault();
+ event.stopPropagation();
});
}
function addClickEventToAllErrorMessages() {
- jQuery('a.afocus').each(function () {
- const target = jQuery(this);
- target.click((evt) => {
- let field = jQuery(`#${target.attr('href')!.substr(1)}`);
- if (field === null) {
- // Cut off '_id' (necessary for select boxes)
- field = jQuery(`#${target.attr('href')!.substr(1).concat('_id')}`);
+ document.querySelectorAll('a.afocus').forEach((anchor) => {
+ anchor.addEventListener('click', function (evt) {
+ evt.preventDefault();
+
+ const href = anchor.getAttribute('href');
+ if (!href?.startsWith('#')) return;
+
+ const id = href.substring(1);
+ let field = document.getElementById(id);
+
+ if (!field) {
+ // Try with `_id` suffix (needed for select boxes)
+ field = document.getElementById(id + '_id');
}
- target.unbind(evt);
- return false;
- });
+
+ if (field) {
+ field.focus();
+ }
+ }, { once: true });
});
}
export function initMainMenuExpandStatus() {
- const wrapper = jQuery('#wrapper');
- const upToggle = jQuery('ul.menu_root.closed li.open a.arrow-left-to-project');
+ const wrapper = document.querySelector('#wrapper')!;
+ const upToggle = document.querySelector('ul.menu_root.closed li.open a.arrow-left-to-project');
- if (upToggle.length === 1 && wrapper.hasClass('hidden-navigation')) {
- upToggle.trigger('click');
+ if (upToggle && wrapper.classList.contains('hidden-navigation')) {
+ upToggle.click();
}
}
-function activateFlash(selector:any) {
- const flashMessages = jQuery(selector);
+function activateFlash(selector:string) {
+ const flashMessages = document.querySelectorAll(selector);
- flashMessages.each((ix, e) => {
- const flashMessage = jQuery(e);
- flashMessage.show();
- });
+ flashMessages.forEach((flashMessage) => { showElement(flashMessage); });
}
export function activateFlashNotice() {
@@ -125,8 +109,8 @@ export function activateFlashError() {
}
export function focusFirstErroneousField() {
- const firstErrorSpan = jQuery('span.errorSpan').first();
- const erroneousInput = firstErrorSpan.find('*').filter(':input');
+ const firstErrorSpan = document.querySelector('span.errorSpan');
+ const erroneousInput = firstErrorSpan?.querySelector('input, select, textarea, button');
- erroneousInput.trigger('focus');
+ erroneousInput?.focus();
}
diff --git a/frontend/src/app/core/setup/globals/global-listeners/top-menu-scroll.ts b/frontend/src/app/core/setup/globals/global-listeners/top-menu-scroll.ts
index 842c1d9ff1e3..a717ac9502d5 100644
--- a/frontend/src/app/core/setup/globals/global-listeners/top-menu-scroll.ts
+++ b/frontend/src/app/core/setup/globals/global-listeners/top-menu-scroll.ts
@@ -38,17 +38,17 @@ export function scrollHeaderOnMobile() {
// Condition needed for safari browser to avoid negative positions
const currentScrollPos = scrollableElement.scrollTop < 0 ? 0 : scrollableElement.scrollTop;
// Only if sidebar is not opened or search bar is opened
- if (!(jQuery('#main').hasClass('hidden-navigation'))
- || jQuery('.op-app-header').hasClass('op-app-header_search-open')
+ if (!(document.querySelector('#main')!.classList.contains('hidden-navigation'))
+ || document.querySelector('.op-app-header')?.classList.contains('op-app-header_search-open')
|| Math.abs(currentScrollPos - prevScrollPos) <= headerHeight) { // to avoid flickering at the end of the page
return;
}
if (prevScrollPos !== undefined && currentScrollPos !== undefined && (prevScrollPos > currentScrollPos)) {
// Slide top menu in or out of viewport and change viewport height
- jQuery('#wrapper').removeClass('_header-scrolled');
+ document.querySelector('#wrapper')!.classList.remove('_header-scrolled');
} else {
- jQuery('#wrapper').addClass('_header-scrolled');
+ document.querySelector('#wrapper')!.classList.add('_header-scrolled');
}
prevScrollPos = currentScrollPos;
});
diff --git a/frontend/src/app/core/setup/globals/onboarding/onboarding_tour.ts b/frontend/src/app/core/setup/globals/onboarding/onboarding_tour.ts
index b299a9094f6c..f6604363719b 100644
--- a/frontend/src/app/core/setup/globals/onboarding/onboarding_tour.ts
+++ b/frontend/src/app/core/setup/globals/onboarding/onboarding_tour.ts
@@ -19,6 +19,7 @@ import { ConfigurationService } from 'core-app/core/config/configuration.service
import 'core-vendor/enjoyhint';
import { wpFullViewOnboardingTourSteps } from 'core-app/core/setup/globals/onboarding/tours/work_package_full_view_tour';
+import { getMetaContent } from '../global-helpers';
declare global {
interface Window {
@@ -46,16 +47,19 @@ function initializeTour(storageValue:string) {
// eslint-disable-next-line @typescript-eslint/no-unsafe-call,@typescript-eslint/no-unsafe-assignment
window.onboardingTourInstance = new window.EnjoyHint({
onStart() {
- jQuery('#content-wrapper, #menu-sidebar').addClass('-hidden-overflow');
+ document.querySelectorAll('#content-wrapper, #menu-sidebar')
+ .forEach((elem) => elem.classList.add('-hidden-overflow'));
sessionStorage.setItem(onboardingTourStorageKey, storageValue);
},
onEnd() {
sessionStorage.setItem(onboardingTourStorageKey, storageValue);
- jQuery('#content-wrapper, #menu-sidebar').removeClass('-hidden-overflow');
+ document.querySelectorAll('#content-wrapper, #menu-sidebar')
+ .forEach((elem) => elem.classList.remove('-hidden-overflow'));
},
onSkip() {
sessionStorage.setItem(onboardingTourStorageKey, 'skipped');
- jQuery('#content-wrapper, #menu-sidebar').removeClass('-hidden-overflow');
+ document.querySelectorAll('#content-wrapper, #menu-sidebar')
+ .forEach((elem) => elem.classList.remove('-hidden-overflow'));
},
});
}
@@ -91,8 +95,8 @@ function workPackageFullViewTour() {
function ganttTour(configuration:ConfigurationService) {
initializeTour('ganttTourFinished');
- const boardsDemoDataAvailable = jQuery('meta[name=boards_demo_data_available]').attr('content') === 'true';
- const teamPlannerDemoDataAvailable = jQuery('meta[name=demo_view_of_type_team_planner_seeded]').attr('content') === 'true';
+ const boardsDemoDataAvailable = getMetaContent('boards_demo_data_available') === 'true';
+ const teamPlannerDemoDataAvailable = getMetaContent('demo_view_of_type_team_planner_seeded') === 'true';
const eeTokenAvailable = configuration.availableFeatures.includes('board_view');
waitForElement('.work-package--results-tbody', '#content', () => {
@@ -121,7 +125,7 @@ function ganttTour(configuration:ConfigurationService) {
function boardTour(configuration:ConfigurationService) {
initializeTour('boardsTourFinished');
- const teamPlannerDemoDataAvailable = jQuery('meta[name=demo_view_of_type_team_planner_seeded]').attr('content') === 'true';
+ const teamPlannerDemoDataAvailable = getMetaContent('demo_view_of_type_team_planner_seeded') === 'true';
const eeTokenAvailable = configuration.availableFeatures.includes('board_view');
waitForElement('wp-single-card', '#content', () => {
diff --git a/frontend/src/app/core/setup/globals/onboarding/onboarding_tour_trigger.ts b/frontend/src/app/core/setup/globals/onboarding/onboarding_tour_trigger.ts
index d709489d7a86..69326dc7c83a 100644
--- a/frontend/src/app/core/setup/globals/onboarding/onboarding_tour_trigger.ts
+++ b/frontend/src/app/core/setup/globals/onboarding/onboarding_tour_trigger.ts
@@ -6,6 +6,7 @@ import {
waitForElement,
} from 'core-app/core/setup/globals/onboarding/helpers';
import { debugLog } from 'core-app/shared/helpers/debug_output';
+import { getMetaContent } from '../global-helpers';
async function triggerTour(name:OnboardingTourNames):Promise {
debugLog(`Loading and triggering onboarding tour ${name}`);
@@ -25,7 +26,7 @@ export function detectOnboardingTour():void {
// ------------------------------- Global -------------------------------
const url = new URL(window.location.href);
const isMobile = document.body.classList.contains('-browser-mobile');
- const demoProjectsAvailable = jQuery('meta[name=demo_projects_available]').attr('content') === 'true';
+ const demoProjectsAvailable = getMetaContent('demo_projects_available') === 'true';
let currentTourPart = sessionStorage.getItem(onboardingTourStorageKey);
let tourCancelled = false;
@@ -49,7 +50,7 @@ export function detectOnboardingTour():void {
}
});
- jQuery('[data-tour-selector="modal-close-button"]')[0].addEventListener('click', () => {
+ document.querySelector('[data-tour-selector="modal-close-button"]')?.addEventListener('click', () => {
tourCancelled = true;
void triggerTour('homescreen');
});
diff --git a/frontend/src/app/core/setup/globals/onboarding/tours/boards_tour.ts b/frontend/src/app/core/setup/globals/onboarding/tours/boards_tour.ts
index a8a62c72185c..3df8bb11ff0e 100644
--- a/frontend/src/app/core/setup/globals/onboarding/tours/boards_tour.ts
+++ b/frontend/src/app/core/setup/globals/onboarding/tours/boards_tour.ts
@@ -30,7 +30,7 @@ export function boardTourSteps(edition:'basic'|'enterprise'):OnboardingStep[] {
nextButton: { text: I18n.t('js.onboarding.buttons.next') },
containerClass: '-dark -hidden-arrow',
onNext() {
- jQuery('[data-tour-selector="main-menu--arrow-left_boards"]')[0].click();
+ document.querySelector('[data-tour-selector="main-menu--arrow-left_boards"]')?.click();
},
},
];
@@ -49,7 +49,7 @@ export function navigateToBoardStep(edition:'basic'|'enterprise'):OnboardingStep
showSkip: false,
nextButton: { text: I18n.t('js.onboarding.buttons.next') },
onNext() {
- jQuery('#boards-wrapper>.boards-menu-item ~ .toggler')[0].click();
+ document.querySelector('#boards-wrapper>.boards-menu-item ~ .toggler')?.click();
waitForElement(
'.op-submenu--item-action',
'#main-menu',
diff --git a/frontend/src/app/core/setup/globals/onboarding/tours/gantt_tour.ts b/frontend/src/app/core/setup/globals/onboarding/tours/gantt_tour.ts
index d8bdf47db37a..94b28abd27bf 100644
--- a/frontend/src/app/core/setup/globals/onboarding/tours/gantt_tour.ts
+++ b/frontend/src/app/core/setup/globals/onboarding/tours/gantt_tour.ts
@@ -13,7 +13,7 @@ export function ganttOnboardingTourSteps():OnboardingStep[] {
showSkip: false,
nextButton: { text: I18n.t('js.onboarding.buttons.next') },
onNext() {
- jQuery('[data-tour-selector="main-menu--arrow-left_gantt"]')[0].click();
+ document.querySelector('[data-tour-selector="main-menu--arrow-left_gantt"]')?.click();
},
},
];
diff --git a/frontend/src/app/core/setup/globals/onboarding/tours/team_planners_tour.ts b/frontend/src/app/core/setup/globals/onboarding/tours/team_planners_tour.ts
index 4a83192669c3..afb5fdd03116 100644
--- a/frontend/src/app/core/setup/globals/onboarding/tours/team_planners_tour.ts
+++ b/frontend/src/app/core/setup/globals/onboarding/tours/team_planners_tour.ts
@@ -45,7 +45,7 @@ export function navigateToTeamPlannerStep():OnboardingStep {
showSkip: false,
nextButton: { text: I18n.t('js.onboarding.buttons.next') },
onNext() {
- jQuery('.team-planner-view-menu-item ~ .toggler')[0].click();
+ document.querySelector('.team-planner-view-menu-item ~ .toggler')?.click();
waitForElement(
'.op-submenu--item-action',
diff --git a/frontend/src/app/core/setup/globals/onboarding/tours/work_package_full_view_tour.ts b/frontend/src/app/core/setup/globals/onboarding/tours/work_package_full_view_tour.ts
index e5a73eea2c7b..01dcde7198c0 100644
--- a/frontend/src/app/core/setup/globals/onboarding/tours/work_package_full_view_tour.ts
+++ b/frontend/src/app/core/setup/globals/onboarding/tours/work_package_full_view_tour.ts
@@ -8,7 +8,7 @@ export function wpFullViewOnboardingTourSteps():OnboardingStep[] {
nextButton: { text: I18n.t('js.onboarding.buttons.next') },
containerClass: '-dark -hidden-arrow',
onNext() {
- jQuery('.main-menu--arrow-left-to-project')[0].click();
+ document.querySelector('.main-menu--arrow-left-to-project')?.click();
},
},
{
@@ -16,7 +16,7 @@ export function wpFullViewOnboardingTourSteps():OnboardingStep[] {
showSkip: false,
nextButton: { text: I18n.t('js.onboarding.buttons.next') },
onNext() {
- jQuery('#main-menu-gantt')[0].click();
+ document.querySelector('#main-menu-gantt')?.click();
},
},
{
diff --git a/frontend/src/app/core/setup/globals/openproject.ts b/frontend/src/app/core/setup/globals/openproject.ts
index c404c4c0ff8c..b56f7cbb2951 100644
--- a/frontend/src/app/core/setup/globals/openproject.ts
+++ b/frontend/src/app/core/setup/globals/openproject.ts
@@ -28,7 +28,7 @@
import { OpenProjectPluginContext } from 'core-app/features/plugins/plugin-context';
import { input, InputState } from '@openproject/reactivestates';
-import { getMetaElement, GlobalHelpers } from 'core-app/core/setup/globals/global-helpers';
+import { getMetaContent, getMetaValue, GlobalHelpers } from 'core-app/core/setup/globals/global-helpers';
import { firstValueFrom } from 'rxjs';
import { ThemeUtils } from './theme-utils';
@@ -70,15 +70,15 @@ export class OpenProject {
}
public get urlRoot():string {
- return getMetaElement('app_base_path')?.content || '';
+ return getMetaContent('app_base_path');
}
public get environment():string {
- return getMetaElement('openproject_initializer')?.dataset.environment || '';
+ return getMetaValue('openproject_initializer', 'environment');
}
public get edition():string {
- return getMetaElement('openproject_initializer')?.dataset.edition || '';
+ return getMetaValue('openproject_initializer', 'edition');
}
public get isStandardEdition():boolean {
diff --git a/frontend/src/app/core/setup/init-locale.ts b/frontend/src/app/core/setup/init-locale.ts
index 7b5646c23eaa..e68ed949b721 100644
--- a/frontend/src/app/core/setup/init-locale.ts
+++ b/frontend/src/app/core/setup/init-locale.ts
@@ -28,14 +28,16 @@
import moment from 'moment';
import { I18n } from 'i18n-js';
+import { getMetaElement } from './globals/global-helpers';
export function initializeLocale() {
- const meta = document.querySelector('meta[name=openproject_initializer]');
- const userLocale = meta?.dataset.locale || 'en';
- const defaultLocale = meta?.dataset.defaultlocale || 'en';
- const instanceLocale = meta?.dataset.instancelocale || 'en';
- const firstDayOfWeek = parseInt(meta?.dataset.firstdayofweek || '', 10); // properties of meta.dataset are exposed in lowercase
- const firstWeekOfYear = parseInt(meta?.dataset.firstweekofyear || '', 10); // properties of meta.dataset are exposed in lowercase
+ const meta = getMetaElement('openproject_initializer');
+ const getInitializerValue = (key:string, defaultValue = '') => meta?.dataset[key] ?? defaultValue;
+ const userLocale = getInitializerValue('locale', 'en');
+ const defaultLocale = getInitializerValue('defaultlocale', 'en');
+ const instanceLocale = getInitializerValue('instancelocale', 'en');
+ const firstDayOfWeek = parseInt(getInitializerValue('firstdayofweek'), 10); // properties of meta.dataset are exposed in lowercase
+ const firstWeekOfYear = parseInt(getInitializerValue('firstweekofyear'), 10); // properties of meta.dataset are exposed in lowercase
const i18n = new I18n();
i18n.locale = userLocale;
diff --git a/frontend/src/app/core/top-menu/top-menu.service.ts b/frontend/src/app/core/top-menu/top-menu.service.ts
index 37d3a1fc6479..d05a13645aa4 100644
--- a/frontend/src/app/core/top-menu/top-menu.service.ts
+++ b/frontend/src/app/core/top-menu/top-menu.service.ts
@@ -31,6 +31,7 @@ import {
Injectable,
} from '@angular/core';
import { DOCUMENT } from '@angular/common';
+import { isVisible } from 'core-app/shared/helpers/dom-helpers';
export const ANIMATION_RATE_MS = 100;
@@ -45,17 +46,15 @@ export class TopMenuService {
private skipContentClickListener():void {
// Skip menu on content
- const skipLink = this.document.querySelector('#skip-navigation--content') as HTMLElement;
+ const skipLink = this.document.querySelector('#skip-navigation--content');
skipLink?.addEventListener('click', () => {
// Skip to the breadcrumb or the first link in the toolbar or the first link in the content (homescreen)
const selectors = '.first-breadcrumb-element a, .toolbar-container a:first-of-type, #content a:first-of-type';
- const visibleLink = jQuery(selectors)
- .not(':hidden')
- .first();
+ const visibleLink = Array
+ .from(document.querySelectorAll(selectors))
+ .find((link) => isVisible(link));
- if (visibleLink.length) {
- visibleLink.trigger('focus');
- }
+ visibleLink?.focus();
});
}
}
diff --git a/frontend/src/app/core/turbo/turbo-requests.service.ts b/frontend/src/app/core/turbo/turbo-requests.service.ts
index 90f2ed387172..71614a5a996f 100644
--- a/frontend/src/app/core/turbo/turbo-requests.service.ts
+++ b/frontend/src/app/core/turbo/turbo-requests.service.ts
@@ -3,6 +3,7 @@ import { renderStreamMessage } from '@hotwired/turbo';
import { ToastService } from 'core-app/shared/components/toaster/toast.service';
import { debugLog } from 'core-app/shared/helpers/debug_output';
import { TurboHelpers } from 'core-turbo/helpers';
+import { getMetaContent } from '../setup/globals/global-helpers';
@Injectable({ providedIn: 'root' })
export class TurboRequestsService {
@@ -35,7 +36,7 @@ export class TurboRequestsService {
'X-Authentication-Scheme': 'Session',
};
if(init.method && !(init.method === 'GET' || init.method === 'HEAD')) {
- defaultHeaders['X-CSRF-Token'] = document.querySelector('meta[name="csrf-token"]')?.content;
+ defaultHeaders['X-CSRF-Token'] = getMetaContent('csrf-token');
}
init.headers = {
diff --git a/frontend/src/app/features/admin/types/type-form-configuration.component.ts b/frontend/src/app/features/admin/types/type-form-configuration.component.ts
index 15d58ccf0bd2..5bd30026a25b 100644
--- a/frontend/src/app/features/admin/types/type-form-configuration.component.ts
+++ b/frontend/src/app/features/admin/types/type-form-configuration.component.ts
@@ -54,9 +54,9 @@ export class TypeFormConfigurationComponent extends UntilDestroyedMixin implemen
private element:HTMLElement;
- private form:JQuery;
+ private form:HTMLFormElement;
- private submit:JQuery;
+ private submit:HTMLButtonElement;
public groups:TypeGroup[] = [];
@@ -68,8 +68,14 @@ export class TypeFormConfigurationComponent extends UntilDestroyedMixin implemen
private no_filter_query:string;
+ private eventListeners = {
+ typeFormUpdater: () => {
+ this.updateHiddenFields();
+ }
+ };
+
constructor(
- private elementRef:ElementRef,
+ private elementRef:ElementRef,
private I18n:I18nService,
private dragula:DragulaService,
private confirmDialog:ConfirmDialogService,
@@ -90,33 +96,29 @@ export class TypeFormConfigurationComponent extends UntilDestroyedMixin implemen
// Hook on form submit
this.element = this.elementRef.nativeElement;
this.no_filter_query = this.element.dataset.noFilterQuery!;
- this.form = jQuery(this.element).closest('form');
- this.submit = this.form.find('.form-configuration--save');
+ this.form = this.element.closest('form')!;
+ this.submit = this.form.querySelector('.form-configuration--save')!;
// In the following we are triggering the form submit ourselves to work around
// a firefox shortcoming. But to avoid double submits which are sometimes not canceled fast
// enough, we need to memoize whether we have already submitted.
let submitted = false;
- this.form.on('submit', () => {
+ this.form.addEventListener('submit', () => {
submitted = true;
});
// Capture mousedown on button because firefox breaks blur on click
- this.submit.on('mousedown', () => {
+ this.submit.addEventListener('mousedown', () => {
setTimeout(() => {
if (!submitted) {
- this.form.trigger('submit');
+ this.form.requestSubmit();
}
}, 50);
- return true;
});
// Capture regular form submit
- this.form.on('submit.typeformupdater', () => {
- this.updateHiddenFields();
- return true;
- });
+ this.form.addEventListener('submit', this.eventListeners.typeFormUpdater);
// Setup groups
this.groupsDrake = this
@@ -161,7 +163,7 @@ export class TypeFormConfigurationComponent extends UntilDestroyedMixin implemen
}
ngAfterViewInit():void {
- const menu = jQuery(this.elementRef.nativeElement).find('.toolbar-items');
+ const menu = this.elementRef.nativeElement.querySelector('.toolbar-items')!;
installMenuLogic(menu);
}
@@ -240,11 +242,12 @@ export class TypeFormConfigurationComponent extends UntilDestroyedMixin implemen
},
})
.then(() => {
- this.form.find('input#type_attribute_groups').val(JSON.stringify([]));
+ const input = this.form.querySelector('input#type_attribute_groups')!;
+ input.value = JSON.stringify([]);
// Disable our form handler that updates the attribute groups
- this.form.off('submit.typeformupdater');
- this.form.trigger('submit');
+ this.form.removeEventListener('submit', this.eventListeners.typeFormUpdater);
+ this.form.requestSubmit();
})
.catch(() => {
});
@@ -268,13 +271,13 @@ export class TypeFormConfigurationComponent extends UntilDestroyedMixin implemen
}
private updateHiddenFields():void {
- const hiddenField = this.form.find('.admin-type-form--hidden-field');
+ const hiddenField = this.form.querySelector('.admin-type-form--hidden-field')!;
if (this.groups.length === 0) {
// Ensure we're adding an empty group if deliberately removing
// all values.
- hiddenField.val(JSON.stringify([this.emptyGroup]));
+ hiddenField.value = JSON.stringify([this.emptyGroup]);
} else {
- hiddenField.val(JSON.stringify(this.groups));
+ hiddenField.value = JSON.stringify(this.groups);
}
}
}
diff --git a/frontend/src/app/features/bim/bcf/bcf-wp-attribute-group/bcf-wp-attribute-group.component.ts b/frontend/src/app/features/bim/bcf/bcf-wp-attribute-group/bcf-wp-attribute-group.component.ts
index fb87f9ea1ab6..b898bfc49821 100644
--- a/frontend/src/app/features/bim/bcf/bcf-wp-attribute-group/bcf-wp-attribute-group.component.ts
+++ b/frontend/src/app/features/bim/bcf/bcf-wp-attribute-group/bcf-wp-attribute-group.component.ts
@@ -296,12 +296,12 @@ export class BcfWpAttributeGroupComponent extends UntilDestroyedMixin implements
// eslint-disable-next-line class-methods-use-this
public galleryPreviewOpen():void {
- jQuery('.op-app-header').addClass('-no-z-index');
+ document.querySelector('.op-app-header')?.classList.add('-no-z-index');
}
// eslint-disable-next-line class-methods-use-this
public galleryPreviewClose():void {
- jQuery('.op-app-header').removeClass('-no-z-index');
+ document.querySelector('.op-app-header')?.classList.remove('-no-z-index');
}
public selectViewpointInGallery() {
diff --git a/frontend/src/app/features/bim/ifc_models/ifc-viewer/ifc-viewer.service.ts b/frontend/src/app/features/bim/ifc_models/ifc-viewer/ifc-viewer.service.ts
index 5db248b92539..14d33fe8f91d 100644
--- a/frontend/src/app/features/bim/ifc_models/ifc-viewer/ifc-viewer.service.ts
+++ b/frontend/src/app/features/bim/ifc_models/ifc-viewer/ifc-viewer.service.ts
@@ -45,6 +45,7 @@ import { BIMViewer } from '@xeokit/xeokit-bim-viewer/dist/xeokit-bim-viewer.es';
import { BcfViewpointData, CreateBcfViewpointData } from 'core-app/features/bim/bcf/api/bcf-api.model';
import { HalResource } from 'core-app/features/hal/resources/hal-resource';
import idFromLink from 'core-app/features/hal/helpers/id-from-link';
+import { getMetaContent } from 'core-app/core/setup/globals/global-helpers';
export interface XeokitElements {
canvasElement:HTMLElement;
@@ -152,7 +153,7 @@ export class IFCViewerService extends ViewerBridgeService {
const formData = new FormData();
formData.append(
'authenticity_token',
- jQuery('meta[name=csrf-token]').attr('content') as string,
+ getMetaContent('csrf-token')
);
formData.append(
'_method',
diff --git a/frontend/src/app/features/bim/ifc_models/toolbar/view-toggle/bcf-view-toggle-dropdown.directive.ts b/frontend/src/app/features/bim/ifc_models/toolbar/view-toggle/bcf-view-toggle-dropdown.directive.ts
index 2cc608d117c2..3778371b6880 100644
--- a/frontend/src/app/features/bim/ifc_models/toolbar/view-toggle/bcf-view-toggle-dropdown.directive.ts
+++ b/frontend/src/app/features/bim/ifc_models/toolbar/view-toggle/bcf-view-toggle-dropdown.directive.ts
@@ -58,7 +58,7 @@ export class BcfViewToggleDropdownDirective extends OpContextMenuTrigger {
super(elementRef, opContextMenu);
}
- protected open(evt:JQuery.TriggeredEvent):void {
+ protected open(evt:Event):void {
this.buildItems();
this.opContextMenu.show(this, evt);
}
diff --git a/frontend/src/app/features/boards/board/add-card-dropdown/add-card-dropdown-menu.directive.ts b/frontend/src/app/features/boards/board/add-card-dropdown/add-card-dropdown-menu.directive.ts
index 8ce936169fbb..7ca7cdeac5c9 100644
--- a/frontend/src/app/features/boards/board/add-card-dropdown/add-card-dropdown-menu.directive.ts
+++ b/frontend/src/app/features/boards/board/add-card-dropdown/add-card-dropdown-menu.directive.ts
@@ -56,28 +56,11 @@ export class AddCardDropdownMenuDirective extends OpContextMenuTrigger {
super(elementRef, opContextMenu);
}
- protected open(evt:JQuery.TriggeredEvent) {
+ protected open(evt:Event) {
this.items = this.buildItems();
this.opContextMenu.show(this, evt);
}
- /**
- * Positioning args for jquery-ui position.
- *
- * @param {Event} openerEvent
- */
- public positionArgs(evt:JQuery.TriggeredEvent) {
- const additionalPositionArgs = {
- my: 'left top',
- at: 'left bottom',
- };
-
- const position = super.positionArgs(evt);
- _.assign(position, additionalPositionArgs);
-
- return position;
- }
-
private buildItems() {
return [
{
diff --git a/frontend/src/app/features/boards/board/board-actions/version/version-action.service.ts b/frontend/src/app/features/boards/board/board-actions/version/version-action.service.ts
index ab2f6a07e372..90992070755f 100644
--- a/frontend/src/app/features/boards/board/board-actions/version/version-action.service.ts
+++ b/frontend/src/app/features/boards/board/board-actions/version/version-action.service.ts
@@ -169,7 +169,7 @@ export class BoardVersionActionService extends CachedBoardActionService {
// Show link
linkText: this.I18n.t('js.boards.version.show_version'),
href: this.pathHelper.versionShowPath(id),
- onClick: (evt:JQuery.TriggeredEvent) => {
+ onClick: (evt) => {
if (!isClickedWithModifier(evt)) {
window.open(this.pathHelper.versionShowPath(id), '_blank');
return true;
@@ -183,7 +183,7 @@ export class BoardVersionActionService extends CachedBoardActionService {
hidden: !version.$links.update,
linkText: this.I18n.t('js.boards.version.edit_version'),
href: this.pathHelper.versionEditPath(id),
- onClick: (evt:JQuery.TriggeredEvent) => {
+ onClick: (evt) => {
if (!isClickedWithModifier(evt)) {
window.open(this.pathHelper.versionEditPath(id), '_blank');
return true;
diff --git a/frontend/src/app/features/boards/board/board-list/board-list.component.ts b/frontend/src/app/features/boards/board/board-list/board-list.component.ts
index bef95fe310fb..a4cee5ecc028 100644
--- a/frontend/src/app/features/boards/board/board-list/board-list.component.ts
+++ b/frontend/src/app/features/boards/board/board-list/board-list.component.ts
@@ -96,7 +96,7 @@ export class BoardListComponent extends AbstractWidgetComponent implements OnIni
@Input() public board:Board;
/** Access to the loading indicator element */
- @ViewChild('loadingIndicator', { static: true }) indicator:ElementRef;
+ @ViewChild('loadingIndicator', { static: true }) indicator:ElementRef;
/** Access to the card view */
@ViewChild(WorkPackageCardViewComponent) cardView:WorkPackageCardViewComponent;
@@ -438,7 +438,7 @@ export class BoardListComponent extends AbstractWidgetComponent implements OnIni
}
private get indicatorInstance() {
- return this.loadingIndicator.indicator(jQuery(this.indicator.nativeElement));
+ return this.loadingIndicator.indicator(this.indicator.nativeElement);
}
private setQueryProps(filters:ApiV3Filter[]) {
diff --git a/frontend/src/app/features/boards/board/configuration-modal/board-configuration.modal.ts b/frontend/src/app/features/boards/board/configuration-modal/board-configuration.modal.ts
index 33b63ad9c52b..0809e4c5911c 100644
--- a/frontend/src/app/features/boards/board/configuration-modal/board-configuration.modal.ts
+++ b/frontend/src/app/features/boards/board/configuration-modal/board-configuration.modal.ts
@@ -56,7 +56,7 @@ export class BoardConfigurationModalComponent extends OpModalComponent implement
}
ngOnInit() {
- this.$element = this.elementRef.nativeElement as HTMLElement;
+ this.element = this.elementRef.nativeElement as HTMLElement;
this.tabPortalHost = new TabPortalOutlet(
this.boardConfigurationService.tabs,
@@ -112,6 +112,6 @@ export class BoardConfigurationModalComponent extends OpModalComponent implement
}
protected get afterFocusOn():HTMLElement {
- return this.$element;
+ return this.element;
}
}
diff --git a/frontend/src/app/features/boards/board/toolbar-menu/boards-toolbar-menu.directive.ts b/frontend/src/app/features/boards/board/toolbar-menu/boards-toolbar-menu.directive.ts
index 70f0a85ae10e..0ecf746de4eb 100644
--- a/frontend/src/app/features/boards/board/toolbar-menu/boards-toolbar-menu.directive.ts
+++ b/frontend/src/app/features/boards/board/toolbar-menu/boards-toolbar-menu.directive.ts
@@ -39,7 +39,7 @@ import { BoardConfigurationModalComponent } from 'core-app/features/boards/board
import { BoardService } from 'core-app/features/boards/board/board.service';
import { StateService } from '@uirouter/core';
import { ToastService } from 'core-app/shared/components/toaster/toast.service';
-import { triggerEditingEvent } from 'core-app/shared/components/editable-toolbar-title/editable-toolbar-title.component';
+import { selectableTitleIdentifier, triggerEditingEvent } from 'core-app/shared/components/editable-toolbar-title/editable-toolbar-title.component';
@Directive({
selector: '[boardsToolbarMenu]',
@@ -69,7 +69,7 @@ export class BoardsToolbarMenuDirective extends OpContextMenuTrigger {
};
}
- protected open(evt:JQuery.TriggeredEvent) {
+ protected open(evt:Event) {
this.buildItems();
this.opContextMenu.show(this, evt);
}
@@ -93,7 +93,7 @@ export class BoardsToolbarMenuDirective extends OpContextMenuTrigger {
icon: 'icon-edit',
onClick: () => {
if (this.board.grid.updateImmediately) {
- jQuery('.toolbar-container .editable-toolbar-title--input').trigger(triggerEditingEvent);
+ document.querySelector(selectableTitleIdentifier)?.dispatchEvent(new CustomEvent(triggerEditingEvent, { bubbles: true }));
}
return true;
diff --git a/frontend/src/app/features/calendar/op-work-packages-calendar.service.ts b/frontend/src/app/features/calendar/op-work-packages-calendar.service.ts
index 59c8356fb790..b05ca8d704ab 100644
--- a/frontend/src/app/features/calendar/op-work-packages-calendar.service.ts
+++ b/frontend/src/app/features/calendar/op-work-packages-calendar.service.ts
@@ -335,7 +335,7 @@ export class OpWorkPackagesCalendarService extends UntilDestroyedMixin {
event.preventDefault();
- const handler = new WorkPackageViewContextMenu(this.injector, workPackageId, jQuery(event.target as HTMLElement));
+ const handler = new WorkPackageViewContextMenu(this.injector, workPackageId, event.target as HTMLElement);
this.contextMenuService.show(handler, event);
}
diff --git a/frontend/src/app/features/calendar/te-calendar/te-calendar.component.ts b/frontend/src/app/features/calendar/te-calendar/te-calendar.component.ts
index b2d64952dfe5..ad891028358f 100644
--- a/frontend/src/app/features/calendar/te-calendar/te-calendar.component.ts
+++ b/frontend/src/app/features/calendar/te-calendar/te-calendar.component.ts
@@ -534,22 +534,23 @@ export class TimeEntryCalendarComponent implements AfterViewInit, OnDestroy {
const schema = (await this.schemaCache.ensureLoaded(entry as TimeEntryResource)) as TimeEntrySchema;
- jQuery(event.el).tooltip({
- content: this.tooltipContentString(event.event.extendedProps.entry as TimeEntryResource, schema),
- items: '.fc-event',
- close() {
- jQuery('.ui-helper-hidden-accessible').remove();
- },
- track: true,
- });
+ const tooltip = document.createElement('tool-tip');
+ tooltip.textContent = this.tooltipContentString(event.event.extendedProps.entry as TimeEntryResource, schema);
+ event.el.appendChild(tooltip);
+
+ // TODO: port tooltips
+ // jQuery(event.el).tooltip({
+ // content: this.tooltipContentString(event.event.extendedProps.entry as TimeEntryResource, schema),
+ // items: '.fc-event',
+ // close() {
+ // document.querySelectorAll('.ui-helper-hidden-accessible').forEach(element => element.remove());
+ // },
+ // track: true,
+ // });
}
private removeTooltip(event:CalendarViewEvent):void {
- const target = jQuery(event.el);
-
- if (target.tooltip('instance')) {
- jQuery(event.el).tooltip('disable');
- }
+ // TODO: port tooltips
}
private prependDuration(event:CalendarViewEvent):void {
@@ -561,9 +562,9 @@ export class TimeEntryCalendarComponent implements AfterViewInit, OnDestroy {
const formattedDuration = this.timezone.formattedDuration(timeEntry.hours as string);
- jQuery(event.el)
- .find('.fc-event-title')
- .prepend(`${formattedDuration}
`);
+ event.el
+ .querySelector('.fc-event-title')
+ ?.insertAdjacentHTML('afterbegin', `${formattedDuration}
`);
}
/* Fade out event text to the bottom to avoid it being cut of weirdly.
@@ -582,19 +583,16 @@ export class TimeEntryCalendarComponent implements AfterViewInit, OnDestroy {
return;
}
- const $element = jQuery(event.el);
- const fadeout = jQuery('
');
+ const element = event.el;
+ const fadeout = document.createElement('div');
+ fadeout.classList.add('fc-fadeout');
const hslaStart = this.colors.toHsla(this.entryName(timeEntry), 0);
const hslaEnd = this.colors.toHsla(this.entryName(timeEntry), 100);
- fadeout.css('background', `-webkit-linear-gradient(${hslaStart} 0%, ${hslaEnd} 100%`);
-
- ['-moz-linear-gradient', '-o-linear-gradient', 'linear-gradient', '-ms-linear-gradient'].forEach((style) => {
- fadeout.css('background-image', `${style}(${hslaStart} 0%, ${hslaEnd} 100%`);
- });
+ fadeout.style.backgroundImage = `linear-gradient(${hslaStart} 0%, ${hslaEnd} 100%)`;
- $element.append(fadeout);
+ element.append(fadeout);
}
private beforeEventRemove(event:CalendarViewEvent):void {
diff --git a/frontend/src/app/features/hal/http/openproject-header-interceptor.ts b/frontend/src/app/features/hal/http/openproject-header-interceptor.ts
index 035a118f32a1..259ca7db3d69 100644
--- a/frontend/src/app/features/hal/http/openproject-header-interceptor.ts
+++ b/frontend/src/app/features/hal/http/openproject-header-interceptor.ts
@@ -3,6 +3,7 @@ import {
} from '@angular/common/http';
import { Observable } from 'rxjs';
import { Injectable } from '@angular/core';
+import { getMetaContent } from 'core-app/core/setup/globals/global-helpers';
export const EXTERNAL_REQUEST_HEADER = 'X-External-Request';
@@ -29,14 +30,14 @@ export class OpenProjectHeaderInterceptor implements HttpInterceptor {
}
private handleAuthenticatedRequest(req:HttpRequest, next:HttpHandler):Observable> {
- const csrf_token:string|undefined = jQuery('meta[name=csrf-token]').attr('content');
+ const csrfToken = getMetaContent('csrf-token');
let newHeaders = req.headers
.set('X-Authentication-Scheme', 'Session')
.set('X-Requested-With', 'XMLHttpRequest');
- if (csrf_token) {
- newHeaders = newHeaders.set('X-CSRF-TOKEN', csrf_token);
+ if (csrfToken) {
+ newHeaders = newHeaders.set('X-CSRF-TOKEN', csrfToken);
}
// Clone the request to add the new header
diff --git a/frontend/src/app/features/work-packages/components/wp-card-view/event-handler/click-handler.ts b/frontend/src/app/features/work-packages/components/wp-card-view/event-handler/click-handler.ts
index 6ffab7b60990..8299a9a2620f 100644
--- a/frontend/src/app/features/work-packages/components/wp-card-view/event-handler/click-handler.ts
+++ b/frontend/src/app/features/work-packages/components/wp-card-view/event-handler/click-handler.ts
@@ -7,6 +7,7 @@ import { WorkPackageCardViewService } from 'core-app/features/work-packages/comp
import { StateService } from '@uirouter/core';
import { DeviceService } from 'core-app/core/browser/device.service';
import { InjectField } from 'core-app/shared/helpers/angular/inject-field.decorator';
+import { EventType } from 'core-app/features/work-packages/routing/wp-view-base/event-handling/event-handler-registry';
export class CardClickHandler implements CardEventHandler {
// Injections
@@ -24,8 +25,8 @@ export class CardClickHandler implements CardEventHandler {
card:WorkPackageCardViewComponent) {
}
- public get EVENT() {
- return 'click.cardView.card';
+ public get EVENT():EventType {
+ return 'click';
}
public get SELECTOR() {
@@ -33,20 +34,20 @@ export class CardClickHandler implements CardEventHandler {
}
public eventScope(card:WorkPackageCardViewComponent) {
- return jQuery(card.container.nativeElement);
+ return card.container.nativeElement;
}
- public handleEvent(card:WorkPackageCardViewComponent, evt:JQuery.TriggeredEvent) {
- const target = jQuery(evt.target);
+ public handleEvent(card:WorkPackageCardViewComponent, evt:MouseEvent) {
+ const target = evt.target as HTMLElement;
// Ignore links
- if (target.is('a') || target.parent().is('a')) {
+ if (target instanceof HTMLAnchorElement || target.parentElement instanceof HTMLAnchorElement) {
return true;
}
// Locate the card from event
- const element = target.closest('wp-single-card');
- const wpId = element.data('workPackageId');
+ const element = target.closest('wp-single-card')!;
+ const wpId = element.dataset.workPackageId;
if (!wpId) {
return true;
@@ -57,14 +58,14 @@ export class CardClickHandler implements CardEventHandler {
return false;
}
- protected handleWorkPackage(card:WorkPackageCardViewComponent, wpId:any, element:JQuery, evt:JQuery.TriggeredEvent) {
+ protected handleWorkPackage(card:WorkPackageCardViewComponent, wpId:any, element:HTMLElement, evt:MouseEvent) {
this.setSelection(card, wpId, element, evt);
card.itemClicked.emit({ workPackageId: wpId, double: false });
}
- protected setSelection(card:WorkPackageCardViewComponent, wpId:string, element:JQuery, evt:JQuery.TriggeredEvent) {
- const classIdentifier = element.data('classIdentifier');
+ protected setSelection(card:WorkPackageCardViewComponent, wpId:string, element:HTMLElement, evt:MouseEvent) {
+ const classIdentifier = element.dataset.classIdentifier!;
const index = this.wpCardView.findRenderedCard(classIdentifier);
// Update single selection if no modifier present
diff --git a/frontend/src/app/features/work-packages/components/wp-card-view/event-handler/double-click-handler.ts b/frontend/src/app/features/work-packages/components/wp-card-view/event-handler/double-click-handler.ts
index bce6b1702d31..de5b50a5b4fb 100644
--- a/frontend/src/app/features/work-packages/components/wp-card-view/event-handler/double-click-handler.ts
+++ b/frontend/src/app/features/work-packages/components/wp-card-view/event-handler/double-click-handler.ts
@@ -4,6 +4,7 @@ import { WorkPackageCardViewComponent } from 'core-app/features/work-packages/co
import { WorkPackageViewSelectionService } from 'core-app/features/work-packages/routing/wp-view-base/view-services/wp-view-selection.service';
import { StateService } from '@uirouter/core';
import { InjectField } from 'core-app/shared/helpers/angular/inject-field.decorator';
+import { EventType } from 'core-app/features/work-packages/routing/wp-view-base/event-handling/event-handler-registry';
export class CardDblClickHandler implements CardEventHandler {
@InjectField() $state:StateService;
@@ -14,8 +15,8 @@ export class CardDblClickHandler implements CardEventHandler {
card:WorkPackageCardViewComponent) {
}
- public get EVENT() {
- return 'dblclick.cardView.card';
+ public get EVENT():EventType {
+ return 'dblclick';
}
public get SELECTOR() {
@@ -23,20 +24,20 @@ export class CardDblClickHandler implements CardEventHandler {
}
public eventScope(card:WorkPackageCardViewComponent) {
- return jQuery(card.container.nativeElement);
+ return card.container.nativeElement;
}
- public handleEvent(card:WorkPackageCardViewComponent, evt:JQuery.TriggeredEvent) {
- const target = jQuery(evt.target);
+ public handleEvent(card:WorkPackageCardViewComponent, evt:Event) {
+ const target = evt.target as HTMLElement;
// Ignore links
- if (target.is('a') || target.parent().is('a')) {
+ if (target instanceof HTMLAnchorElement || target.parentElement instanceof HTMLAnchorElement) {
return true;
}
// Locate the row from event
- const element = target.closest('wp-single-card');
- const wpId = element.data('workPackageId');
+ const element = target.closest('wp-single-card')!;
+ const wpId = element.dataset.workPackageId;
if (!wpId) {
return true;
diff --git a/frontend/src/app/features/work-packages/components/wp-card-view/event-handler/right-click-handler.ts b/frontend/src/app/features/work-packages/components/wp-card-view/event-handler/right-click-handler.ts
index 422095a4f53f..4f09c8f5a3cf 100644
--- a/frontend/src/app/features/work-packages/components/wp-card-view/event-handler/right-click-handler.ts
+++ b/frontend/src/app/features/work-packages/components/wp-card-view/event-handler/right-click-handler.ts
@@ -8,6 +8,7 @@ import { WorkPackageCardViewService } from 'core-app/features/work-packages/comp
import { OPContextMenuService } from 'core-app/shared/components/op-context-menu/op-context-menu.service';
import { WorkPackageViewContextMenu } from 'core-app/shared/components/op-context-menu/wp-context-menu/wp-view-context-menu.directive';
import { InjectField } from 'core-app/shared/helpers/angular/inject-field.decorator';
+import { EventType } from 'core-app/features/work-packages/routing/wp-view-base/event-handling/event-handler-registry';
export class CardRightClickHandler implements CardEventHandler {
// Injections
@@ -21,8 +22,8 @@ export class CardRightClickHandler implements CardEventHandler {
card:WorkPackageCardViewComponent) {
}
- public get EVENT() {
- return 'contextmenu.cardView.rightclick';
+ public get EVENT():EventType {
+ return 'contextmenu'; // N.B.: contextmenu is not supported by Safari on iOS.
}
public get SELECTOR() {
@@ -30,15 +31,15 @@ export class CardRightClickHandler implements CardEventHandler {
}
public eventScope(card:WorkPackageCardViewComponent) {
- return jQuery(card.container.nativeElement);
+ return card.container.nativeElement;
}
- public handleEvent(card:WorkPackageCardViewComponent, evt:JQuery.TriggeredEvent) {
- const target = jQuery(evt.target);
+ public handleEvent(card:WorkPackageCardViewComponent, evt:Event) {
+ const target = evt.target as HTMLElement;
// We want to keep the original context menu on hrefs
// (currently, this is only the id)
- if (target.closest(`.${uiStateLinkClass}`).length) {
+ if (target.closest(`.${uiStateLinkClass}`)) {
debugLog('Allowing original context menu on state link');
return true;
}
@@ -47,20 +48,20 @@ export class CardRightClickHandler implements CardEventHandler {
evt.stopPropagation();
// Locate the card from event
- const element = target.closest('wp-single-card');
- const wpId = element.data('workPackageId');
+ const element = target.closest('wp-single-card')!;
+ const wpId = element.dataset.workPackageId;
if (!wpId) {
return true;
}
- const classIdentifier = element.data('classIdentifier');
+ const classIdentifier = element.dataset.classIdentifier!;
const index = this.wpCardView.findRenderedCard(classIdentifier);
if (!this.wpTableSelection.isSelected(wpId)) {
this.wpTableSelection.setSelection(wpId, index);
}
- const handler = new WorkPackageViewContextMenu(this.injector, wpId, jQuery(evt.target) as JQuery, {}, card.showInfoButton);
+ const handler = new WorkPackageViewContextMenu(this.injector, wpId, evt.target as HTMLElement, {}, card.showInfoButton);
this.opContextMenu.show(handler, evt);
return false;
diff --git a/frontend/src/app/features/work-packages/components/wp-edit-form/table-edit-form.ts b/frontend/src/app/features/work-packages/components/wp-edit-form/table-edit-form.ts
index 51680f83e495..ed4af6aba7a9 100644
--- a/frontend/src/app/features/work-packages/components/wp-edit-form/table-edit-form.ts
+++ b/frontend/src/app/features/work-packages/components/wp-edit-form/table-edit-form.ts
@@ -93,12 +93,12 @@ export class TableEditForm extends EditForm {
this.resourceSubscription.unsubscribe();
}
- public findContainer(fieldName:string):JQuery {
- return this.rowContainer.find(`.${tdClassName}.${fieldName} .${editFieldContainerClass}`).first();
+ public findContainer(fieldName:string) {
+ return this.rowContainer?.querySelector(`.${tdClassName}.${fieldName} .${editFieldContainerClass}`);
}
public findCell(fieldName:string) {
- return this.rowContainer.find(`.${tdClassName}.${fieldName}`).first();
+ return this.rowContainer?.querySelector(`.${tdClassName}.${fieldName}`);
}
public activateField(form:EditForm, schema:IFieldSchema, fieldName:string, errors:string[]):Promise {
@@ -107,12 +107,12 @@ export class TableEditForm extends EditForm {
// Forcibly set the width since the edit field may otherwise
// be given more width. Thereby preserve a minimum width of 150.
// To avoid flickering content, the padding is removed, too.
- const td = this.findCell(fieldName);
- td.addClass(editModeClassName);
- let width = parseInt(td.css('width'));
+ const td = this.findCell(fieldName)!;
+ td.classList.add(editModeClassName);
+ let width = td.offsetWidth;
width = width > 150 ? width - 10 : 150;
- td.css('max-width', `${width}px`);
- td.css('width', `${width}px`);
+ td.style.maxWidth = `${width}px`;
+ td.style.width = `${width}px`;
return this.editingPortalService.create(
cell,
@@ -127,16 +127,16 @@ export class TableEditForm extends EditForm {
public reset(fieldName:string, focus?:boolean) {
const cell = this.findContainer(fieldName);
- const td = this.findCell(fieldName);
+ const td = this.findCell(fieldName)!;
- if (cell.length) {
- this.findCell(fieldName).css('width', '');
- this.findCell(fieldName).css('max-width', '');
- this.cellBuilder.refresh(cell[0], this.resource, fieldName);
- td.removeClass(editModeClassName);
+ if (cell) {
+ td.style.width = '';
+ td.style.maxWidth = '';
+ this.cellBuilder.refresh(cell, this.resource, fieldName);
+ td.classList.remove(editModeClassName);
if (focus) {
- this.FocusHelper.focus(cell[0]);
+ this.FocusHelper.focus(cell);
}
}
}
@@ -154,10 +154,9 @@ export class TableEditForm extends EditForm {
protected focusOnFirstError():void {
// Focus the first field that is erroneous
- jQuery(this.table.tableAndTimelineContainer)
- .find(`.${activeFieldContainerClassName}.-error .${activeFieldClassName}`)
- .first()
- .trigger('focus');
+ this.table.tableAndTimelineContainer
+ ?.querySelector(`.${activeFieldContainerClassName}.-error .${activeFieldClassName}`)
+ ?.focus();
}
/**
@@ -181,15 +180,15 @@ export class TableEditForm extends EditForm {
const interval = setInterval(() => {
const container = this.findContainer(fieldName);
- if (container.length > 0) {
+ if (container) {
clearInterval(interval);
- resolve(container[0]);
+ resolve(container);
}
}, 100);
});
}
private get rowContainer() {
- return jQuery(this.table.tableAndTimelineContainer).find(`.${this.classIdentifier}-table`);
+ return this.table.tableAndTimelineContainer.querySelector(`.${this.classIdentifier}-table`);
}
}
diff --git a/frontend/src/app/features/work-packages/components/wp-edit/wp-edit-field/wp-replacement-label.component.ts b/frontend/src/app/features/work-packages/components/wp-edit/wp-edit-field/wp-replacement-label.component.ts
index 16d52c7e6f80..64f133d7ba50 100644
--- a/frontend/src/app/features/work-packages/components/wp-edit/wp-edit-field/wp-replacement-label.component.ts
+++ b/frontend/src/app/features/work-packages/components/wp-edit/wp-edit-field/wp-replacement-label.component.ts
@@ -39,20 +39,20 @@ import { EditFormComponent } from 'core-app/shared/components/fields/edit/edit-f
export class WorkPackageReplacementLabelComponent implements OnInit {
@Input('fieldName') public fieldName:string;
- private $element:JQuery;
+ private element:HTMLElement;
constructor(protected wpeditForm:EditFormComponent,
protected elementRef:ElementRef) {
}
ngOnInit() {
- this.$element = jQuery(this.elementRef.nativeElement);
+ this.element = this.elementRef.nativeElement;
}
- public activate(evt:JQuery.TriggeredEvent) {
+ public activate(evt:Event) {
// Skip clicks on help texts
- const target = jQuery(evt.target);
- if (target.closest('.help-text--entry').length) {
+ const target = evt.target as HTMLElement;
+ if (target.closest('.help-text--entry')) {
return true;
}
diff --git a/frontend/src/app/features/work-packages/components/wp-fast-table/builders/modes/grouped/grouped-rows-builder.ts b/frontend/src/app/features/work-packages/components/wp-fast-table/builders/modes/grouped/grouped-rows-builder.ts
index 2a36ad1b6f52..374f6c9cc2d0 100644
--- a/frontend/src/app/features/work-packages/components/wp-fast-table/builders/modes/grouped/grouped-rows-builder.ts
+++ b/frontend/src/app/features/work-packages/components/wp-fast-table/builders/modes/grouped/grouped-rows-builder.ts
@@ -15,6 +15,7 @@ import { RowsBuilder } from '../rows-builder';
import { GroupHeaderBuilder } from './group-header-builder';
import { GroupedRenderPass } from './grouped-render-pass';
import { groupedRowClassName, groupIdentifier } from './grouped-rows-helpers';
+import { getNodeIndex } from 'core-app/shared/helpers/dom-helpers';
export class GroupedRowsBuilder extends RowsBuilder {
// Injections
@@ -70,10 +71,10 @@ export class GroupedRowsBuilder extends RowsBuilder {
const rendered = this.querySpace.tableRendered.value!;
const builder = new GroupHeaderBuilder(this.injector);
- jQuery(this.workPackageTable.tableAndTimelineContainer)
- .find(`.${rowGroupClassName}`)
- .each((i:number, oldRow:Element) => {
- const groupIndex = jQuery(oldRow).data('groupIndex');
+ this.workPackageTable.tableAndTimelineContainer
+ .querySelectorAll(`.${rowGroupClassName}`)
+ .forEach((oldRow) => {
+ const groupIndex = parseInt(oldRow.dataset.groupIndex || '', 10);
const group = groups[groupIndex];
// Refresh the group header
@@ -84,14 +85,17 @@ export class GroupedRowsBuilder extends RowsBuilder {
}
// Set expansion state of contained rows
- const affected = jQuery(this.workPackageTable.tableAndTimelineContainer)
- .find(`.${groupedRowClassName(groupIndex)}`);
- affected.toggleClass(collapsedRowClass, !!group.collapsed);
+ const affected = Array.from(this.workPackageTable.tableAndTimelineContainer
+ .querySelectorAll(`.${groupedRowClassName(groupIndex)}`));
+
+ affected.forEach((el) => el.classList.toggle(collapsedRowClass, !!group.collapsed));
// Update the hidden section of the rendered state
- affected.filter(`.${tableRowClassName}`).each((i, el) => {
+ affected
+ .filter((el) => el.matches(`.${tableRowClassName}`))
+ .forEach((el) => {
// Get the index of this row
- const index = jQuery(el).index();
+ const index = getNodeIndex(el);
// Update the hidden state
rendered[index].hidden = !!group.collapsed;
diff --git a/frontend/src/app/features/work-packages/components/wp-fast-table/builders/modes/hierarchy/single-hierarchy-row-builder.ts b/frontend/src/app/features/work-packages/components/wp-fast-table/builders/modes/hierarchy/single-hierarchy-row-builder.ts
index 1214003a6b43..aa0292f14ab3 100644
--- a/frontend/src/app/features/work-packages/components/wp-fast-table/builders/modes/hierarchy/single-hierarchy-row-builder.ts
+++ b/frontend/src/app/features/work-packages/components/wp-fast-table/builders/modes/hierarchy/single-hierarchy-row-builder.ts
@@ -50,10 +50,10 @@ export class SingleHierarchyRowBuilder extends SingleRowBuilder {
* Refresh a single row after structural changes.
* Remembers and re-adds the hierarchy indicator if necessary.
*/
- public refreshRow(workPackage:WorkPackageResource, jRow:JQuery):JQuery {
+ public refreshRow(workPackage:WorkPackageResource, row:HTMLTableRowElement) {
// Remove any old hierarchy
- const newRow = super.refreshRow(workPackage, jRow);
- newRow.find('.wp-table--hierarchy-span').remove();
+ const newRow = super.refreshRow(workPackage, row);
+ newRow.querySelector('.wp-table--hierarchy-span')?.remove();
this.appendHierarchyIndicator(workPackage, newRow);
return newRow;
@@ -67,7 +67,7 @@ export class SingleHierarchyRowBuilder extends SingleRowBuilder {
const [classes, hidden] = this.ancestorRowData(workPackage);
element.classList.add(...classes);
- this.appendHierarchyIndicator(workPackage, jQuery(element));
+ this.appendHierarchyIndicator(workPackage, element);
return [element, hidden];
}
@@ -114,28 +114,32 @@ export class SingleHierarchyRowBuilder extends SingleRowBuilder {
/**
* Append to the row of hierarchy level a hierarchy indicator.
* @param workPackage
- * @param jRow jQuery row element
+ * @param row row element
* @param level Indentation level
*/
- private appendHierarchyIndicator(workPackage:WorkPackageResource, jRow:JQuery, level?:number):void {
+ private appendHierarchyIndicator(workPackage:WorkPackageResource, row:HTMLTableRowElement, level?:number):void {
const ancestors = workPackage.getAncestors();
const hierarchyLevel = level === undefined || null ? ancestors.length : level;
- const hierarchyElement = this.buildHierarchyIndicator(workPackage, jRow, hierarchyLevel);
+ const hierarchyElement = this.buildHierarchyIndicator(workPackage, row, hierarchyLevel);
- jRow.find('td.subject')
- .addClass('-with-hierarchy')
- .prepend(hierarchyElement);
+ const subjectCell = row.querySelector('td.subject');
+ if (!subjectCell) return;
+
+ subjectCell.classList.add('-with-hierarchy');
+ subjectCell.prepend(hierarchyElement);
// Assure that the content is still visible when the hierarchy indentation is very large
- jRow.find('td.subject').css('minWidth', `${125 + (hierarchyIndentation * hierarchyLevel)}px`);
- jRow.find('td.subject .wp-table--cell-container')
- .css('width', `calc(100% - ${hierarchyBaseIndentation}px - ${hierarchyIndentation * hierarchyLevel}px)`);
+ subjectCell.style.minWidth = `${125 + (hierarchyIndentation * hierarchyLevel)}px`;
+ const container = subjectCell.querySelector('.wp-table--cell-container');
+ if (container) {
+ container.style.width = `calc(100% - ${hierarchyBaseIndentation}px - ${hierarchyIndentation * hierarchyLevel}px)`;
+ }
}
/**
* Build the hierarchy indicator at the given indentation level.
*/
- private buildHierarchyIndicator(workPackage:WorkPackageResource, jRow:JQuery|null, level:number):HTMLElement {
+ private buildHierarchyIndicator(workPackage:WorkPackageResource, row:HTMLTableRowElement, level:number):HTMLElement {
const hierarchyIndicator = document.createElement('span');
const collapsed = this.wpTableHierarchies.collapsed(workPackage.id!);
const indicatorWidth = `${hierarchyBaseIndentation + (hierarchyIndentation * level)}px`;
diff --git a/frontend/src/app/features/work-packages/components/wp-fast-table/builders/primary-render-pass.ts b/frontend/src/app/features/work-packages/components/wp-fast-table/builders/primary-render-pass.ts
index fdc24d8f55cb..3cb396b5ebdf 100644
--- a/frontend/src/app/features/work-packages/components/wp-fast-table/builders/primary-render-pass.ts
+++ b/frontend/src/app/features/work-packages/components/wp-fast-table/builders/primary-render-pass.ts
@@ -14,6 +14,8 @@ import { WorkPackageTable } from '../wp-fast-table';
import {
ChildRelationsRenderPass,
} from 'core-app/features/work-packages/components/wp-fast-table/builders/relations/child-relations-render-pass';
+import { getNodeIndex } from 'core-app/shared/helpers/dom-helpers';
+import invariant from 'tiny-invariant';
export type RenderedRowType = 'primary'|'relations'|'child_relations';
@@ -116,8 +118,8 @@ public readonly injector:Injector,
* @param row
*/
public refresh(row:RowRenderInfo, workPackage:WorkPackageResource, body:HTMLElement) {
- const oldRow = jQuery(body).find(`.${row.classIdentifier}`);
- let replacement:JQuery|null = null;
+ const oldRow = body.querySelector(`.${row.classIdentifier}`)!;
+ let replacement:HTMLElement|null = null;
switch (row.renderType) {
case 'relations':
@@ -131,7 +133,7 @@ public readonly injector:Injector,
break;
}
- if (replacement !== null && oldRow.length) {
+ if (replacement !== null && oldRow) {
oldRow.replaceWith(replacement);
}
}
@@ -150,17 +152,19 @@ public readonly injector:Injector,
* 1. Insert into the document fragment after the last match of the selector
* 2. Splice into the renderedOrder array.
*/
- public spliceRow(row:HTMLElement, selector:string, renderedInfo:RowRenderInfo) {
+ public spliceRow(row:HTMLTableRowElement, selector:string, renderedInfo:RowRenderInfo) {
// Insert into table using the selector
+ const matches = this.tableBody.querySelectorAll(selector);
+ invariant(matches.length, `No matches found for selector: ${selector}`);
+
// If it matches multiple, select the last element
- const target = jQuery(this.tableBody)
- .find(selector)
- .last();
+ const target = matches[matches.length - 1];
- target.after(row);
+ // Insert the new row AFTER the target
+ target.parentNode!.insertBefore(row, target.nextSibling);
// Splice the renderedOrder at this exact location
- const index = target.index();
+ const index = getNodeIndex(target!);
this.renderedOrder.splice(index + 1, 0, renderedInfo);
}
diff --git a/frontend/src/app/features/work-packages/components/wp-fast-table/builders/relations/relation-row-builder.ts b/frontend/src/app/features/work-packages/components/wp-fast-table/builders/relations/relation-row-builder.ts
index 4cac3c2fe15b..a0ca0c73f1ac 100644
--- a/frontend/src/app/features/work-packages/components/wp-fast-table/builders/relations/relation-row-builder.ts
+++ b/frontend/src/app/features/work-packages/components/wp-fast-table/builders/relations/relation-row-builder.ts
@@ -37,7 +37,7 @@ public readonly injector:Injector,
* @param column
* @return {any}
*/
- public buildCell(workPackage:WorkPackageResource, column:QueryColumn):HTMLElement|null {
+ public buildCell(workPackage:WorkPackageResource, column:QueryColumn):HTMLTableCellElement|null {
// handle relation types
if (isRelationColumn(column)) {
return this.emptyRelationCell(column);
@@ -49,7 +49,7 @@ public readonly injector:Injector,
/**
* Build the columns on the given empty row
*/
- public buildEmptyRelationRow(from:WorkPackageResource, to:WorkPackageResource):[HTMLElement, WorkPackageResource] {
+ public buildEmptyRelationRow(from:WorkPackageResource, to:WorkPackageResource):[HTMLTableRowElement, WorkPackageResource] {
// Let the primary row builder build the row
const row = this.createEmptyRelationRow(from, to);
const [tr] = super.buildEmptyRow(to, row);
@@ -87,12 +87,12 @@ relationGroupClass(from.id!),
/**
*
- * @param jRow
+ * @param row
* @param typeLabel
* @param columnId
*/
public appendRelationLabel(
- jRow:JQuery,
+ row:HTMLTableRowElement,
typeLabel:string,
columnId:string,
):void {
@@ -100,8 +100,8 @@ relationGroupClass(from.id!),
relationLabel.classList.add('relation-row--type-label', 'badge');
relationLabel.textContent = typeLabel;
- jRow.find(`.${relationCellClassName}`).empty();
- jRow.find(`.${relationCellClassName}.${columnId}`).append(relationLabel);
+ row.querySelector(`.${relationCellClassName}`)!.innerHTML = '';
+ row.querySelector(`.${relationCellClassName}.${columnId}`)!.append(relationLabel);
}
protected emptyRelationCell(column:QueryColumn) {
diff --git a/frontend/src/app/features/work-packages/components/wp-fast-table/builders/relations/relations-render-pass.ts b/frontend/src/app/features/work-packages/components/wp-fast-table/builders/relations/relations-render-pass.ts
index 55831f98f108..a0502809e957 100644
--- a/frontend/src/app/features/work-packages/components/wp-fast-table/builders/relations/relations-render-pass.ts
+++ b/frontend/src/app/features/work-packages/components/wp-fast-table/builders/relations/relations-render-pass.ts
@@ -93,7 +93,7 @@ export class RelationsRenderPass {
}
protected renderRelationRow(
- relationRow:HTMLElement,
+ relationRow:HTMLTableRowElement,
row:RowRenderInfo,
label:string,
column:QueryColumn,
@@ -103,7 +103,7 @@ export class RelationsRenderPass {
) {
relationRow.classList.add(...row.additionalClasses);
this.relationRowBuilder.appendRelationLabel(
- jQuery(relationRow),
+ relationRow,
label,
column.id,
);
@@ -134,7 +134,7 @@ export class RelationsRenderPass {
public refreshRelationRow(
renderedRow:RelationRenderInfo,
workPackage:WorkPackageResource,
- oldRow:JQuery,
+ oldRow:HTMLTableRowElement,
) {
const newRow = this.relationRowBuilder.refreshRow(workPackage, oldRow);
this.relationRowBuilder.appendRelationLabel(
diff --git a/frontend/src/app/features/work-packages/components/wp-fast-table/builders/rows/single-row-builder.ts b/frontend/src/app/features/work-packages/components/wp-fast-table/builders/rows/single-row-builder.ts
index 89638411457e..f82de0af6c6b 100644
--- a/frontend/src/app/features/work-packages/components/wp-fast-table/builders/rows/single-row-builder.ts
+++ b/frontend/src/app/features/work-packages/components/wp-fast-table/builders/rows/single-row-builder.ts
@@ -92,7 +92,7 @@ export class SingleRowBuilder {
return columns;
}
- public buildCell(workPackage:WorkPackageResource, column:QueryColumn):HTMLElement|null {
+ public buildCell(workPackage:WorkPackageResource, column:QueryColumn):HTMLTableCellElement|null {
// handle relation types
if (isRelationColumn(column)) {
return this.relationCellBuilder.build(workPackage, column);
@@ -175,23 +175,24 @@ export class SingleRowBuilder {
/**
* Refresh a row that is currently being edited, that is, some edit fields may be open
*/
- public refreshRow(workPackage:WorkPackageResource, jRow:JQuery):JQuery {
+ public refreshRow(workPackage:WorkPackageResource, row:HTMLTableRowElement):HTMLTableRowElement {
// Detach all current edit cells
- const cells = jRow.find(`.${tdClassName}`).detach();
+ const cells = Array.from(row.querySelectorAll(`.${tdClassName}`))
+ .map((el) => el.parentNode!.removeChild(el));
// Remember the order of all new edit cells
- const newCells:HTMLElement[] = [];
+ const newCells:HTMLTableCellElement[] = [];
this.augmentedColumns.forEach((column:QueryColumn) => {
- const oldTd = cells.filter(`td.${column.id}`);
+ const oldTd = cells.find((cell) => cell.matches(`td.${column.id}`));
// Treat internal columns specially
// and skip the replacement of the column if this is being edited.
// But only do that, if the column existed before. Sometimes, e.g. when lacking permissions
// the column was not correctly created (with the intended classes). This code then
// increases the robustness.
- if ((column.id.startsWith('__internal') || this.isColumnBeingEdited(workPackage, column)) && oldTd.length) {
- newCells.push(oldTd[0]);
+ if ((column.id.startsWith('__internal') || this.isColumnBeingEdited(workPackage, column)) && oldTd) {
+ newCells.push(oldTd);
return;
}
@@ -203,8 +204,8 @@ export class SingleRowBuilder {
}
});
- jRow.prepend(newCells);
- return jRow;
+ row.prepend(...newCells);
+ return row;
}
protected isColumnBeingEdited(workPackage:WorkPackageResource, column:QueryColumn) {
@@ -215,24 +216,27 @@ export class SingleRowBuilder {
protected buildEmptyRow(workPackage:WorkPackageResource, row:HTMLTableRowElement):[HTMLTableRowElement, boolean] {
const change = this.workPackageTable.editing.change(workPackage);
- const cells:{ [attribute:string]:JQuery } = {};
+ const cells:Record = {};
if (change && !change.isEmpty()) {
// Try to find an old instance of this row
const oldRow = locateTableRowByIdentifier(this.classIdentifier(workPackage));
change.changedAttributes.forEach((attribute:string) => {
- cells[attribute] = oldRow.find(`.${tdClassName}.${attribute}`);
+ const oldCell = oldRow?.querySelector(`.${tdClassName}.${attribute}`);
+ if (oldCell) {
+ cells[attribute] = oldCell;
+ }
});
}
this.augmentedColumns.forEach((column:QueryColumn) => {
let cell:Element|null;
- const oldCell:JQuery|undefined = cells[column.id];
+ const oldCell = cells[column.id];
- if (oldCell && oldCell.length) {
+ if (oldCell) {
debugLog(`Rendering previous open column ${column.id} on ${workPackage.id}`);
- jQuery(row).append(oldCell);
+ row.appendChild(oldCell);
} else {
cell = this.buildCell(workPackage, column);
diff --git a/frontend/src/app/features/work-packages/components/wp-fast-table/builders/table-action-renderer.ts b/frontend/src/app/features/work-packages/components/wp-fast-table/builders/table-action-renderer.ts
index 8f3e7787a6c4..fe8a78cd59d8 100644
--- a/frontend/src/app/features/work-packages/components/wp-fast-table/builders/table-action-renderer.ts
+++ b/frontend/src/app/features/work-packages/components/wp-fast-table/builders/table-action-renderer.ts
@@ -13,7 +13,7 @@ export class TableActionRenderer {
constructor(public readonly injector:Injector) {
}
- public build(workPackage:WorkPackageResource):HTMLElement {
+ public build(workPackage:WorkPackageResource):HTMLTableCellElement{
// Append details button
const td = document.createElement('td');
td.classList.add(tdClassName, contextMenuTdClassName, internalContextMenuColumn.id, 'hide-when-print');
diff --git a/frontend/src/app/features/work-packages/components/wp-fast-table/handlers/cell/edit-cell-handler.ts b/frontend/src/app/features/work-packages/components/wp-fast-table/handlers/cell/edit-cell-handler.ts
index 7371d5991a0d..378dbbf74eb1 100644
--- a/frontend/src/app/features/work-packages/components/wp-fast-table/handlers/cell/edit-cell-handler.ts
+++ b/frontend/src/app/features/work-packages/components/wp-fast-table/handlers/cell/edit-cell-handler.ts
@@ -10,6 +10,7 @@ import { TableEventComponent, TableEventHandler } from '../table-handler-registr
import { ClickOrEnterHandler } from '../click-or-enter-handler';
import { WorkPackageTable } from '../../wp-fast-table';
import { tableRowClassName } from '../../builders/rows/single-row-builder';
+import { EventType } from 'core-app/features/work-packages/routing/wp-view-base/event-handling/event-handler-registry';
export class EditCellHandler extends ClickOrEnterHandler implements TableEventHandler {
// Injections
@@ -19,8 +20,8 @@ export class EditCellHandler extends ClickOrEnterHandler implements TableEventHa
// Keep a reference to all
- public get EVENT() {
- return 'click.table.cell, keydown.table.cell';
+ public get EVENT():EventType[] {
+ return ['click', 'keydown'];
}
public get SELECTOR() {
@@ -28,21 +29,21 @@ export class EditCellHandler extends ClickOrEnterHandler implements TableEventHa
}
public eventScope(view:TableEventComponent) {
- return jQuery(view.workPackageTable.tableAndTimelineContainer);
+ return view.workPackageTable.tableAndTimelineContainer;
}
constructor(public readonly injector:Injector) {
super();
}
- protected processEvent(table:WorkPackageTable, evt:JQuery.TriggeredEvent):void {
+ protected processEvent(table:WorkPackageTable, evt:MouseEvent|KeyboardEvent):void {
debugLog('Starting editing on cell: ', evt.target);
evt.preventDefault();
// Locate the cell from event
- const target = jQuery(evt.target).closest(`.${displayClassName}`);
+ const target = (evt.target as HTMLElement).closest(`.${displayClassName}`);
// Get the target field name
- const fieldName = target.data('fieldName');
+ const fieldName = target?.dataset.fieldName;
if (!fieldName) {
debugLog('Click handled by cell not a field? ', evt.target);
@@ -50,18 +51,21 @@ export class EditCellHandler extends ClickOrEnterHandler implements TableEventHa
}
// Locate the row
- const rowElement = target.closest(`.${tableRowClassName}`);
+ const rowElement = target.closest(`.${tableRowClassName}`)!;
// Get the work package we're editing
- const workPackageId = rowElement.data('workPackageId');
+ const workPackageId = rowElement.dataset.workPackageId!;
const workPackage = this.states.workPackages.get(workPackageId).value!;
// Get the row context
- const classIdentifier = rowElement.data('classIdentifier');
+ const classIdentifier = rowElement.dataset.classIdentifier!;
// Get any existing edit state for this work package
const form = table.editing.startEditing(workPackage, classIdentifier);
- // Get the position where the user clicked.
- const positionOffset = getPosition(evt);
+ let positionOffset = 0;
+ if (evt.type === 'click') {
+ // Get the position where the user clicked.
+ positionOffset = getPosition(evt as MouseEvent);
+ }
// Activate the field
form.activate(fieldName)
@@ -69,6 +73,6 @@ export class EditCellHandler extends ClickOrEnterHandler implements TableEventHa
handler.$onUserActivate.next();
handler.focus(positionOffset);
})
- .catch(() => target.addClass(readOnlyClassName));
+ .catch(() => target.classList.add(readOnlyClassName));
}
}
diff --git a/frontend/src/app/features/work-packages/components/wp-fast-table/handlers/cell/relations-cell-handler.ts b/frontend/src/app/features/work-packages/components/wp-fast-table/handlers/cell/relations-cell-handler.ts
index eb6529f27f23..3cb0e34707b6 100644
--- a/frontend/src/app/features/work-packages/components/wp-fast-table/handlers/cell/relations-cell-handler.ts
+++ b/frontend/src/app/features/work-packages/components/wp-fast-table/handlers/cell/relations-cell-handler.ts
@@ -7,13 +7,14 @@ import { tableRowClassName } from '../../builders/rows/single-row-builder';
import { WorkPackageTable } from '../../wp-fast-table';
import { ClickOrEnterHandler } from '../click-or-enter-handler';
import { TableEventComponent, TableEventHandler } from '../table-handler-registry';
+import { EventType } from 'core-app/features/work-packages/routing/wp-view-base/event-handling/event-handler-registry';
export class RelationsCellHandler extends ClickOrEnterHandler implements TableEventHandler {
// Injections
@InjectField() wpTableRelationColumns:WorkPackageViewRelationColumnsService;
- public get EVENT() {
- return 'click.table.relationsCell, keydown.table.relationsCell';
+ public get EVENT():EventType[] {
+ return ['click', 'keydown'];
}
public get SELECTOR() {
@@ -21,24 +22,24 @@ export class RelationsCellHandler extends ClickOrEnterHandler implements TableEv
}
public eventScope(view:TableEventComponent) {
- return jQuery(view.workPackageTable.tableAndTimelineContainer);
+ return view.workPackageTable.tableAndTimelineContainer;
}
constructor(public readonly injector:Injector) {
super();
}
- protected processEvent(table:WorkPackageTable, evt:JQuery.TriggeredEvent):void {
+ protected processEvent(table:WorkPackageTable, evt:MouseEvent|KeyboardEvent):void {
debugLog('Handled click on relation cell %o', evt.target);
evt.preventDefault();
// Locate the relation td
- const td = jQuery(evt.target).closest(`.${relationCellTdClassName}`);
- const columnId = td.data('columnId');
+ const td = (evt.target as HTMLElement).closest(`.${relationCellTdClassName}`);
+ const columnId = td?.dataset.columnId ?? '';
// Locate the row
- const rowElement = jQuery(evt.target).closest(`.${tableRowClassName}`);
- const workPackageId = rowElement.data('workPackageId');
+ const rowElement = (evt.target as HTMLElement).closest(`.${tableRowClassName}`);
+ const workPackageId = rowElement?.dataset.workPackageId ?? '';
// If currently expanded
if (this.wpTableRelationColumns.getExpandFor(workPackageId) === columnId) {
diff --git a/frontend/src/app/features/work-packages/components/wp-fast-table/handlers/click-or-enter-handler.ts b/frontend/src/app/features/work-packages/components/wp-fast-table/handlers/click-or-enter-handler.ts
index a5f6df36c1e0..fc06dd23250e 100644
--- a/frontend/src/app/features/work-packages/components/wp-fast-table/handlers/click-or-enter-handler.ts
+++ b/frontend/src/app/features/work-packages/components/wp-fast-table/handlers/click-or-enter-handler.ts
@@ -2,18 +2,18 @@ import { TableEventComponent } from 'core-app/features/work-packages/components/
import { WorkPackageTable } from '../wp-fast-table';
/**
- * Execute the callback if the given JQuery Event is either an ENTER key or a click
+ * Execute the callback if the given Event is either an ENTER key or a click
*/
-export function onClickOrEnter(evt:JQuery.TriggeredEvent, callback:() => void) {
- if (evt.type === 'click' || (evt.type === 'keydown' && evt.key === 'Enter')) {
+export function onClickOrEnter(evt:Event, callback:() => void) {
+ if (evt.type === 'click' || (evt.type === 'keydown' && (evt as KeyboardEvent).key === 'Enter')) {
callback();
}
}
export abstract class ClickOrEnterHandler {
- public handleEvent(view:TableEventComponent, evt:JQuery.TriggeredEvent) {
+ public handleEvent(view:TableEventComponent, evt:MouseEvent|KeyboardEvent) {
onClickOrEnter(evt, () => this.processEvent(view.workPackageTable, evt));
}
- protected abstract processEvent(table:WorkPackageTable, evt:JQuery.TriggeredEvent):void;
+ protected abstract processEvent(table:WorkPackageTable, evt:MouseEvent|KeyboardEvent):void;
}
diff --git a/frontend/src/app/features/work-packages/components/wp-fast-table/handlers/context-menu/context-menu-click-handler.ts b/frontend/src/app/features/work-packages/components/wp-fast-table/handlers/context-menu/context-menu-click-handler.ts
index e8bc59e63a1b..0f3e30435fb6 100644
--- a/frontend/src/app/features/work-packages/components/wp-fast-table/handlers/context-menu/context-menu-click-handler.ts
+++ b/frontend/src/app/features/work-packages/components/wp-fast-table/handlers/context-menu/context-menu-click-handler.ts
@@ -4,26 +4,27 @@ import { contextMenuLinkClassName } from 'core-app/features/work-packages/compon
import { TableEventComponent } from 'core-app/features/work-packages/components/wp-fast-table/handlers/table-handler-registry';
import { uiStateLinkClass } from '../../builders/ui-state-link-builder';
import { ContextMenuHandler } from './context-menu-handler';
+import { EventType } from 'core-app/features/work-packages/routing/wp-view-base/event-handling/event-handler-registry';
export class ContextMenuClickHandler extends ContextMenuHandler {
constructor(public readonly injector:Injector) {
super(injector);
}
- public get EVENT() {
- return 'click.table.contextmenu';
+ public get EVENT():EventType {
+ return 'click';
}
public get SELECTOR() {
return `.${contextMenuLinkClassName}`;
}
- public handleEvent(view:TableEventComponent, evt:JQuery.TriggeredEvent):boolean {
- const target = jQuery(evt.target);
+ public handleEvent(view:TableEventComponent, evt:Event):boolean {
+ const target = evt.target as HTMLElement;
// We want to keep the original context menu on hrefs
// (currently, this is only the id
- if (target.closest(`.${uiStateLinkClass}`).length) {
+ if (target.closest(`.${uiStateLinkClass}`)) {
debugLog('Allowing original context menu on state link');
return true;
}
@@ -32,8 +33,8 @@ export class ContextMenuClickHandler extends ContextMenuHandler {
evt.stopPropagation();
// Locate the row from event
- const element = target.closest(this.rowSelector);
- const wpId = element.data('workPackageId');
+ const element = target.closest(this.rowSelector);
+ const wpId = element?.dataset.workPackageId;
if (wpId) {
this.openContextMenu(view.workPackageTable, evt, wpId);
diff --git a/frontend/src/app/features/work-packages/components/wp-fast-table/handlers/context-menu/context-menu-handler.ts b/frontend/src/app/features/work-packages/components/wp-fast-table/handlers/context-menu/context-menu-handler.ts
index cb913ced64b1..d4165f067745 100644
--- a/frontend/src/app/features/work-packages/components/wp-fast-table/handlers/context-menu/context-menu-handler.ts
+++ b/frontend/src/app/features/work-packages/components/wp-fast-table/handlers/context-menu/context-menu-handler.ts
@@ -5,6 +5,8 @@ import { InjectField } from 'core-app/shared/helpers/angular/inject-field.decora
import { tableRowClassName } from '../../builders/rows/single-row-builder';
import { WorkPackageTable } from '../../wp-fast-table';
import { TableEventComponent, TableEventHandler } from '../table-handler-registry';
+import { PositionArgs } from 'core-app/shared/components/op-context-menu/wp-context-menu/wp-view-context-menu.directive';
+import { EventType } from 'core-app/features/work-packages/routing/wp-view-base/event-handling/event-handler-registry';
export abstract class ContextMenuHandler implements TableEventHandler {
// Injections
@@ -17,18 +19,18 @@ export abstract class ContextMenuHandler implements TableEventHandler {
return `.${tableRowClassName}`;
}
- public abstract get EVENT():string;
+ public abstract get EVENT():EventType|EventType[];
public abstract get SELECTOR():string;
public eventScope(view:TableEventComponent) {
- return jQuery(view.workPackageTable.tableAndTimelineContainer);
+ return view.workPackageTable.tableAndTimelineContainer;
}
- public abstract handleEvent(view:TableEventComponent, evt:JQuery.TriggeredEvent):boolean;
+ public abstract handleEvent(view:TableEventComponent, evt:Event):boolean;
- protected openContextMenu(table:WorkPackageTable, evt:JQuery.TriggeredEvent, workPackageId:string, positionArgs?:any):void {
- const handler = new WorkPackageTableContextMenu(this.injector, workPackageId, jQuery(evt.target) as JQuery, positionArgs, table);
+ protected openContextMenu(table:WorkPackageTable, evt:Event, workPackageId:string, positionArgs:PositionArgs = {}):void {
+ const handler = new WorkPackageTableContextMenu(this.injector, workPackageId, evt.target as HTMLElement, positionArgs, table);
this.opContextMenu.show(handler, evt);
}
}
diff --git a/frontend/src/app/features/work-packages/components/wp-fast-table/handlers/context-menu/context-menu-keyboard-handler.ts b/frontend/src/app/features/work-packages/components/wp-fast-table/handlers/context-menu/context-menu-keyboard-handler.ts
index 105268851a4e..fa2eb45b05c8 100644
--- a/frontend/src/app/features/work-packages/components/wp-fast-table/handlers/context-menu/context-menu-keyboard-handler.ts
+++ b/frontend/src/app/features/work-packages/components/wp-fast-table/handlers/context-menu/context-menu-keyboard-handler.ts
@@ -1,26 +1,27 @@
import { Injector } from '@angular/core';
import { TableEventComponent } from 'core-app/features/work-packages/components/wp-fast-table/handlers/table-handler-registry';
import { ContextMenuHandler } from './context-menu-handler';
+import { EventType } from 'core-app/features/work-packages/routing/wp-view-base/event-handling/event-handler-registry';
export class ContextMenuKeyboardHandler extends ContextMenuHandler {
constructor(public readonly injector:Injector) {
super(injector);
}
- public get EVENT() {
- return 'keydown.table.contextmenu';
+ public get EVENT():EventType {
+ return 'keydown';
}
public get SELECTOR() {
return this.rowSelector;
}
- public handleEvent(component:TableEventComponent, evt:JQuery.TriggeredEvent):boolean {
+ public handleEvent(component:TableEventComponent, evt:KeyboardEvent):boolean {
if (!component.workPackageTable.configuration.contextMenuEnabled) {
return false;
}
- const target = jQuery(evt.target);
+ const target = evt.target as HTMLElement;
if (!(evt.key === 'F10' && evt.shiftKey && evt.altKey)) {
return true;
@@ -30,13 +31,16 @@ export class ContextMenuKeyboardHandler extends ContextMenuHandler {
evt.stopPropagation();
// Locate the row from event
- const element = target.closest(this.SELECTOR);
- const wpId = element.data('workPackageId');
-
- // Set position args to open at element
- const position = { my: 'left top', at: 'left bottom', of: target };
-
- super.openContextMenu(component.workPackageTable, evt, wpId, position);
+ const element = target.closest(this.SELECTOR)!;
+ const wpId = element.dataset.workPackageId!;
+
+ super.openContextMenu(
+ component.workPackageTable,
+ evt,
+ wpId,
+ // Set position args to open at element
+ { placement: 'bottom-start', reference: target }
+ );
return false;
}
diff --git a/frontend/src/app/features/work-packages/components/wp-fast-table/handlers/context-menu/context-menu-rightclick-handler.ts b/frontend/src/app/features/work-packages/components/wp-fast-table/handlers/context-menu/context-menu-rightclick-handler.ts
index 32b5e5f32b1d..7806fd131c4f 100644
--- a/frontend/src/app/features/work-packages/components/wp-fast-table/handlers/context-menu/context-menu-rightclick-handler.ts
+++ b/frontend/src/app/features/work-packages/components/wp-fast-table/handlers/context-menu/context-menu-rightclick-handler.ts
@@ -7,6 +7,7 @@ import { tableRowClassName } from '../../builders/rows/single-row-builder';
import { timelineCellClassName } from '../../builders/timeline/timeline-row-builder';
import { uiStateLinkClass } from '../../builders/ui-state-link-builder';
import { ContextMenuHandler } from './context-menu-handler';
+import { EventType } from 'core-app/features/work-packages/routing/wp-view-base/event-handling/event-handler-registry';
export class ContextMenuRightClickHandler extends ContextMenuHandler {
@InjectField() readonly wpTableSelection:WorkPackageViewSelectionService;
@@ -15,8 +16,8 @@ export class ContextMenuRightClickHandler extends ContextMenuHandler {
super(injector);
}
- public get EVENT() {
- return 'contextmenu.table.rightclick';
+ public get EVENT():EventType {
+ return 'contextmenu'; // N.B.: contextmenu is not supported by Safari on iOS.
}
public get SELECTOR() {
@@ -24,18 +25,18 @@ export class ContextMenuRightClickHandler extends ContextMenuHandler {
}
public eventScope(view:TableEventComponent) {
- return jQuery(view.workPackageTable.tableAndTimelineContainer);
+ return view.workPackageTable.tableAndTimelineContainer;
}
- public handleEvent(view:TableEventComponent, evt:JQuery.TriggeredEvent):boolean {
+ public handleEvent(view:TableEventComponent, evt:Event):boolean {
if (!view.workPackageTable.configuration.contextMenuEnabled) {
return false;
}
- const target = jQuery(evt.target);
+ const target = evt.target as HTMLElement;
// We want to keep the original context menu on hrefs
// (currently, this is only the id
- if (target.closest(`.${uiStateLinkClass}`).length) {
+ if (target.closest(`.${uiStateLinkClass}`)) {
debugLog('Allowing original context menu on state link');
return true;
}
@@ -44,8 +45,8 @@ export class ContextMenuRightClickHandler extends ContextMenuHandler {
evt.stopPropagation();
// Locate the row from event
- const element = target.closest(this.SELECTOR);
- const wpId = element.data('workPackageId');
+ const element = target.closest(this.SELECTOR);
+ const wpId = element?.dataset.workPackageId;
if (wpId) {
const [index] = view.workPackageTable.findRenderedRow(wpId);
diff --git a/frontend/src/app/features/work-packages/components/wp-fast-table/handlers/row/click-handler.ts b/frontend/src/app/features/work-packages/components/wp-fast-table/handlers/row/click-handler.ts
index ca6db7eb4475..63060fbed8a8 100644
--- a/frontend/src/app/features/work-packages/components/wp-fast-table/handlers/row/click-handler.ts
+++ b/frontend/src/app/features/work-packages/components/wp-fast-table/handlers/row/click-handler.ts
@@ -10,6 +10,7 @@ import { debugLog } from 'core-app/shared/helpers/debug_output';
import { TableEventComponent, TableEventHandler } from '../table-handler-registry';
import { tableRowClassName } from '../../builders/rows/single-row-builder';
import { KeepTabService } from '../../../wp-single-view-tabs/keep-tab/keep-tab.service';
+import { EventType } from 'core-app/features/work-packages/routing/wp-view-base/event-handling/event-handler-registry';
export class RowClickHandler implements TableEventHandler {
// Injections
@@ -26,8 +27,8 @@ export class RowClickHandler implements TableEventHandler {
constructor(public readonly injector:Injector) {
}
- public get EVENT() {
- return 'click.table.row';
+ public get EVENT():EventType {
+ return 'click';
}
public get SELECTOR() {
@@ -35,28 +36,28 @@ export class RowClickHandler implements TableEventHandler {
}
public eventScope(view:TableEventComponent) {
- return jQuery(view.workPackageTable.tbody);
+ return view.workPackageTable.tbody;
}
- public handleEvent(view:TableEventComponent, evt:JQuery.TriggeredEvent) {
- const target = jQuery(evt.target);
+ public handleEvent(view:TableEventComponent, evt:MouseEvent) {
+ const target = evt.target as HTMLElement;
// Ignore links
- if (target.is('a') || target.parent().is('a')) {
+ if (target instanceof HTMLAnchorElement || target.parentElement instanceof HTMLAnchorElement) {
return true;
}
// Shortcut to any clicks within a cell
// We don't want to handle these.
- if (target.hasClass(`${displayClassName}`) || target.hasClass(`${activeFieldClassName}`)) {
+ if (target.classList.contains(`${displayClassName}`) || target.classList.contains(`${activeFieldClassName}`)) {
debugLog('Skipping click on inner cell');
return true;
}
// Locate the row from event
- const element = target.closest(this.SELECTOR);
- const wpId = element.data('workPackageId');
- const classIdentifier = element.data('classIdentifier');
+ const element = target.closest(this.SELECTOR)!;
+ const wpId = element.dataset.workPackageId;
+ const classIdentifier = element.dataset.classIdentifier!;
if (!wpId) {
return true;
diff --git a/frontend/src/app/features/work-packages/components/wp-fast-table/handlers/row/double-click-handler.ts b/frontend/src/app/features/work-packages/components/wp-fast-table/handlers/row/double-click-handler.ts
index 3d4ec9f5ea41..7763f1988347 100644
--- a/frontend/src/app/features/work-packages/components/wp-fast-table/handlers/row/double-click-handler.ts
+++ b/frontend/src/app/features/work-packages/components/wp-fast-table/handlers/row/double-click-handler.ts
@@ -11,6 +11,7 @@ import { InjectField } from 'core-app/shared/helpers/angular/inject-field.decora
import { TableEventComponent, TableEventHandler } from '../table-handler-registry';
import { tableRowClassName } from '../../builders/rows/single-row-builder';
import { tdClassName } from '../../builders/cell-builder';
+import { EventType } from 'core-app/features/work-packages/routing/wp-view-base/event-handling/event-handler-registry';
export class RowDoubleClickHandler implements TableEventHandler {
// Injections
@@ -25,8 +26,8 @@ export class RowDoubleClickHandler implements TableEventHandler {
constructor(public readonly injector:Injector) {
}
- public get EVENT() {
- return 'dblclick.table.row';
+ public get EVENT():EventType {
+ return 'dblclick';
}
public get SELECTOR() {
@@ -34,11 +35,11 @@ export class RowDoubleClickHandler implements TableEventHandler {
}
public eventScope(view:TableEventComponent) {
- return jQuery(view.workPackageTable.tbody);
+ return view.workPackageTable.tbody;
}
- public handleEvent(view:TableEventComponent, evt:JQuery.TriggeredEvent) {
- const target = jQuery(evt.target);
+ public handleEvent(view:TableEventComponent, evt:MouseEvent) {
+ const target = evt.target as HTMLElement;
// Skip clicks with modifiers
if (isClickedWithModifier(evt)) {
@@ -47,17 +48,17 @@ export class RowDoubleClickHandler implements TableEventHandler {
// Shortcut to any clicks within a cell
// We don't want to handle these.
- if (target.hasClass(`${displayClassName}`) || target.hasClass(`${activeFieldClassName}`)) {
+ if (target.classList.contains(`${displayClassName}`) || target.classList.contains(`${activeFieldClassName}`)) {
debugLog('Skipping click on inner cell');
return true;
}
// Locate the row from event
- const element = target.closest(this.SELECTOR).closest(`.${tableRowClassName}`);
- const wpId = element.data('workPackageId');
+ const element = target.closest(this.SELECTOR)!.closest(`.${tableRowClassName}`)!;
+ const wpId = element.dataset.workPackageId!;
// Ignore links
- if (target.is('a') || target.parent().is('a')) {
+ if (target instanceof HTMLAnchorElement || target.parentElement instanceof HTMLAnchorElement) {
return true;
}
diff --git a/frontend/src/app/features/work-packages/components/wp-fast-table/handlers/row/group-row-handler.ts b/frontend/src/app/features/work-packages/components/wp-fast-table/handlers/row/group-row-handler.ts
index 71031d0888ab..798ad03866a9 100644
--- a/frontend/src/app/features/work-packages/components/wp-fast-table/handlers/row/group-row-handler.ts
+++ b/frontend/src/app/features/work-packages/components/wp-fast-table/handlers/row/group-row-handler.ts
@@ -4,6 +4,7 @@ import { rowGroupClassName } from 'core-app/features/work-packages/components/wp
import { InjectField } from 'core-app/shared/helpers/angular/inject-field.decorator';
import { WorkPackageViewCollapsedGroupsService } from 'core-app/features/work-packages/routing/wp-view-base/view-services/wp-view-collapsed-groups.service';
import { TableEventComponent, TableEventHandler } from '../table-handler-registry';
+import { EventType } from 'core-app/features/work-packages/routing/wp-view-base/event-handling/event-handler-registry';
export class GroupRowHandler implements TableEventHandler {
// Injections
@@ -14,8 +15,8 @@ export class GroupRowHandler implements TableEventHandler {
constructor(public readonly injector:Injector) {
}
- public get EVENT() {
- return 'click.table.groupheader';
+ public get EVENT():EventType {
+ return 'click';
}
public get SELECTOR() {
@@ -23,15 +24,15 @@ export class GroupRowHandler implements TableEventHandler {
}
public eventScope(view:TableEventComponent) {
- return jQuery(view.workPackageTable.tbody);
+ return view.workPackageTable.tbody;
}
- public handleEvent(view:TableEventComponent, evt:JQuery.TriggeredEvent) {
+ public handleEvent(view:TableEventComponent, evt:Event) {
evt.preventDefault();
evt.stopPropagation();
- const groupHeader = jQuery(evt.target).parents(`.${rowGroupClassName}`);
- const groupIdentifier = groupHeader.data('groupIdentifier');
+ const groupHeader = (evt.target as HTMLElement).closest(`.${rowGroupClassName}`);
+ const groupIdentifier = groupHeader?.dataset.groupIdentifier ?? '';
this.workPackageViewCollapsedGroupsService.toggleGroupCollapseState(groupIdentifier);
}
diff --git a/frontend/src/app/features/work-packages/components/wp-fast-table/handlers/row/hierarchy-click-handler.ts b/frontend/src/app/features/work-packages/components/wp-fast-table/handlers/row/hierarchy-click-handler.ts
index 7c1fc79632e4..bee49c72d1cc 100644
--- a/frontend/src/app/features/work-packages/components/wp-fast-table/handlers/row/hierarchy-click-handler.ts
+++ b/frontend/src/app/features/work-packages/components/wp-fast-table/handlers/row/hierarchy-click-handler.ts
@@ -6,6 +6,7 @@ import { InjectField } from 'core-app/shared/helpers/angular/inject-field.decora
import { tableRowClassName } from '../../builders/rows/single-row-builder';
import { WorkPackageTable } from '../../wp-fast-table';
import { ClickOrEnterHandler } from '../click-or-enter-handler';
+import { EventType } from 'core-app/features/work-packages/routing/wp-view-base/event-handling/event-handler-registry';
export class HierarchyClickHandler extends ClickOrEnterHandler implements TableEventHandler {
// Injections
@@ -17,8 +18,8 @@ export class HierarchyClickHandler extends ClickOrEnterHandler implements TableE
super();
}
- public get EVENT() {
- return 'click.table.hierarchy';
+ public get EVENT():EventType {
+ return 'click';
}
public get SELECTOR() {
@@ -26,15 +27,15 @@ export class HierarchyClickHandler extends ClickOrEnterHandler implements TableE
}
public eventScope(view:TableEventComponent) {
- return jQuery(view.workPackageTable.tbody);
+ return view.workPackageTable.tbody;
}
- public processEvent(table:WorkPackageTable, evt:JQuery.TriggeredEvent):void {
- const target = jQuery(evt.target);
+ public processEvent(table:WorkPackageTable, evt:MouseEvent|KeyboardEvent):void {
+ const target = evt.target as HTMLElement;
// Locate the row from event
- const element = target.closest(`.${tableRowClassName}`);
- const wpId = element.data('workPackageId');
+ const element = target.closest(`.${tableRowClassName}`);
+ const wpId = element?.dataset.workPackageId ?? '';
this.wpTableHierarchies.toggle(wpId);
diff --git a/frontend/src/app/features/work-packages/components/wp-fast-table/handlers/row/wp-state-links-handler.ts b/frontend/src/app/features/work-packages/components/wp-fast-table/handlers/row/wp-state-links-handler.ts
index f566f71a38af..b6373346d06b 100644
--- a/frontend/src/app/features/work-packages/components/wp-fast-table/handlers/row/wp-state-links-handler.ts
+++ b/frontend/src/app/features/work-packages/components/wp-fast-table/handlers/row/wp-state-links-handler.ts
@@ -9,6 +9,7 @@ import { KeepTabService } from '../../../wp-single-view-tabs/keep-tab/keep-tab.s
import { tableRowClassName } from '../../builders/rows/single-row-builder';
import { uiStateLinkClass } from '../../builders/ui-state-link-builder';
import { TableEventComponent, TableEventHandler } from '../table-handler-registry';
+import { EventType } from 'core-app/features/work-packages/routing/wp-view-base/event-handling/event-handler-registry';
export class WorkPackageStateLinksHandler implements TableEventHandler {
// Injections
@@ -25,8 +26,8 @@ export class WorkPackageStateLinksHandler implements TableEventHandler {
constructor(public readonly injector:Injector) {
}
- public get EVENT() {
- return 'click.table.wpLink';
+ public get EVENT():EventType {
+ return 'click';
}
public get SELECTOR() {
@@ -34,12 +35,12 @@ export class WorkPackageStateLinksHandler implements TableEventHandler {
}
public eventScope(view:TableEventComponent) {
- return jQuery(view.workPackageTable.tableAndTimelineContainer);
+ return view.workPackageTable.tableAndTimelineContainer;
}
protected workPackage:WorkPackageResource;
- public handleEvent(view:TableEventComponent, evt:JQuery.TriggeredEvent) {
+ public handleEvent(view:TableEventComponent, evt:KeyboardEvent) {
evt.stopPropagation();
// Avoid the state capture when clicking with modifier to allow browser opening in new tab
diff --git a/frontend/src/app/features/work-packages/components/wp-fast-table/handlers/state/hierarchy-transformer.ts b/frontend/src/app/features/work-packages/components/wp-fast-table/handlers/state/hierarchy-transformer.ts
index 365cec2355e0..cdffbbc64b2a 100644
--- a/frontend/src/app/features/work-packages/components/wp-fast-table/handlers/state/hierarchy-transformer.ts
+++ b/frontend/src/app/features/work-packages/components/wp-fast-table/handlers/state/hierarchy-transformer.ts
@@ -15,6 +15,7 @@ import { indicatorCollapsedClass } from 'core-app/features/work-packages/compone
import { tableRowClassName } from 'core-app/features/work-packages/components/wp-fast-table/builders/rows/single-row-builder';
import { WorkPackageViewHierarchies } from 'core-app/features/work-packages/routing/wp-view-base/view-services/wp-table-hierarchies';
import { InjectField } from 'core-app/shared/helpers/angular/inject-field.decorator';
+import { getNodeIndex } from 'core-app/shared/helpers/dom-helpers';
export class HierarchyTransformer {
@InjectField() public wpTableHierarchies:WorkPackageViewHierarchiesService;
@@ -61,7 +62,11 @@ export class HierarchyTransformer {
const rendered = this.querySpace.tableRendered.value!;
// Show all hierarchies
- jQuery('[class^="__hierarchy-group-"]').removeClass((i:number, classNames:string):string => (classNames.match(/__collapsed-group-\d+/g) || []).join(' '));
+ document.querySelectorAll('[class^="__hierarchy-group-"]').forEach((el) => {
+ Array.from(el.classList)
+ .filter((className) => /__collapsed-group-\d+/g.test(className))
+ .forEach((className) => el.classList.remove(className));
+ });
// Mark which rows were hidden by some other hierarchy group
// (e.g., by a collapsed parent)
@@ -70,7 +75,7 @@ export class HierarchyTransformer {
// Hide all collapsed hierarchies
_.each(state.collapsed, (isCollapsed:boolean, wpId:string) => {
// Toggle the root style
- jQuery(`.${hierarchyRootClass(wpId)} .wp-table--hierarchy-indicator`).toggleClass(indicatorCollapsedClass, isCollapsed);
+ document.querySelector(`.${hierarchyRootClass(wpId)} .wp-table--hierarchy-indicator`)?.classList.toggle(indicatorCollapsedClass, isCollapsed);
// Get parent row and mark/unmark it as collapsed
const hierarchyRoot = document.querySelector(`.wp-timeline-cell.__hierarchy-root-${wpId}`);
@@ -84,15 +89,17 @@ export class HierarchyTransformer {
}
// Get all affected children rows
- const affected = jQuery(`.${hierarchyGroupClass(wpId)}`);
+ const affected = Array.from(document.querySelectorAll(`.${hierarchyGroupClass(wpId)}`));
// Hide/Show the descendants.
- affected.toggleClass(collapsedGroupClass(wpId), isCollapsed);
+ affected.forEach((el) => el.classList.toggle(collapsedGroupClass(wpId), isCollapsed));
// Update the hidden section of the rendered state
- affected.filter(`.${tableRowClassName}`).each((i, el) => {
+ affected
+ .filter((el) => el.matches(`.${tableRowClassName}`))
+ .forEach((el) => {
// Get the index of this row
- const index = jQuery(el).index();
+ const index = getNodeIndex(el);
// Update the hidden state
if (!collapsed[index]) {
diff --git a/frontend/src/app/features/work-packages/components/wp-fast-table/handlers/state/selection-transformer.ts b/frontend/src/app/features/work-packages/components/wp-fast-table/handlers/state/selection-transformer.ts
index 0bc61c2daeb0..fba47c3097df 100644
--- a/frontend/src/app/features/work-packages/components/wp-fast-table/handlers/state/selection-transformer.ts
+++ b/frontend/src/app/features/work-packages/components/wp-fast-table/handlers/state/selection-transformer.ts
@@ -32,9 +32,9 @@ export class SelectionTransformer {
.subscribe(() => {
this.wpTableFocus.ifShouldFocus((wpId:string) => {
const element = locateTableRow(wpId);
- if (element.length) {
+ if (element) {
scrollTableRowIntoView(wpId);
- this.FocusHelper.focus(element[0]);
+ this.FocusHelper.focus(element);
}
});
});
@@ -56,12 +56,14 @@ export class SelectionTransformer {
* Update all currently visible rows to match the selection state.
*/
private renderSelectionState(state:WorkPackageViewSelectionState) {
- const context = jQuery(this.table.tableAndTimelineContainer);
+ const context = this.table.tableAndTimelineContainer;
- context.find(`.${tableRowClassName}.${checkedClassName}`).removeClass(checkedClassName);
+ context.querySelectorAll(`.${tableRowClassName}.${checkedClassName}`).forEach((el) => el.classList.remove(checkedClassName));
_.each(state.selected, (selected:boolean, workPackageId:any) => {
- context.find(`.${tableRowClassName}[data-work-package-id="${workPackageId}"]`).toggleClass(checkedClassName, selected);
+ context.querySelectorAll(`.${tableRowClassName}[data-work-package-id="${workPackageId}"]`).forEach((el) => {
+ el.classList.toggle(checkedClassName, selected);
+ });
});
}
}
diff --git a/frontend/src/app/features/work-packages/components/wp-fast-table/handlers/state/timeline-transformer.ts b/frontend/src/app/features/work-packages/components/wp-fast-table/handlers/state/timeline-transformer.ts
index 55bfb83dbea5..3247d6555486 100644
--- a/frontend/src/app/features/work-packages/components/wp-fast-table/handlers/state/timeline-transformer.ts
+++ b/frontend/src/app/features/work-packages/components/wp-fast-table/handlers/state/timeline-transformer.ts
@@ -27,8 +27,8 @@ export class TimelineTransformer {
* Update all currently visible rows to match the selection state.
*/
private renderVisibility(visible:boolean) {
- const container = jQuery(this.table.tableAndTimelineContainer).parent();
- container.find('.work-packages-tabletimeline--timeline-side').toggle(visible);
- container.find('.work-packages-tabletimeline--table-side').toggleClass('-timeline-visible', visible);
+ const container = this.table.tableAndTimelineContainer.parentElement!;
+ container.querySelectorAll('.work-packages-tabletimeline--timeline-side, .work-packages-tabletimeline--table-side')
+ .forEach((sideEl) => sideEl.classList.toggle('-timeline-visible', visible));
}
}
diff --git a/frontend/src/app/features/work-packages/components/wp-fast-table/helpers/wp-table-row-helpers.ts b/frontend/src/app/features/work-packages/components/wp-fast-table/helpers/wp-table-row-helpers.ts
index 7a264294c174..899b634cc60e 100644
--- a/frontend/src/app/features/work-packages/components/wp-fast-table/helpers/wp-table-row-helpers.ts
+++ b/frontend/src/app/features/work-packages/components/wp-fast-table/helpers/wp-table-row-helpers.ts
@@ -11,12 +11,12 @@ export function relationRowClass():string {
return 'wp-table--relations-additional-row';
}
-export function locateTableRow(workPackageId:string):JQuery {
- return jQuery(`.${rowId(workPackageId)}`);
+export function locateTableRow(workPackageId:string) {
+ return document.querySelector(`.${rowId(workPackageId)}`);
}
export function locateTableRowByIdentifier(identifier:string) {
- return jQuery(`.${identifier}-table`);
+ return document.querySelector(`.${identifier}-table`);
}
export function isInsideCollapsedGroup(el?:Element | null) {
@@ -42,20 +42,39 @@ export function locatePredecessorBySelector(el:HTMLElement, selector:string):HTM
export function scrollTableRowIntoView(workPackageId:string):void {
try {
- const element = locateTableRow(workPackageId);
- const container = element.scrollParent()!;
- const containerTop = container.scrollTop()!;
- const containerBottom = containerTop + container.height()!;
+ const element = locateTableRow(workPackageId)!;
+ const container = getScrollParent(element);
+ const containerTop = container.scrollTop;
+ const containerBottom = containerTop + container.clientHeight;
- const elemTop = element[0].offsetTop;
- const elemBottom = elemTop + element.height()!;
+ const elemTop = element.offsetTop;
+ const elemBottom = elemTop + element.offsetHeight;
if (elemTop < containerTop) {
- container[0].scrollTop = elemTop;
+ container.scrollTop = elemTop;
} else if (elemBottom > containerBottom) {
- container[0].scrollTop = elemBottom - container.height()!;
+ container.scrollTop = elemBottom - container.clientHeight;
}
} catch (e) {
console.warn(`Can't scroll row element into view: ${e}`);
}
}
+
+function getScrollParent(element:HTMLElement, includeHidden = false) {
+ const overflowRegex = includeHidden ? /(auto|scroll|hidden)/ : /(auto|scroll)/;
+
+ let parent:HTMLElement|null = element.parentElement;
+
+ while (parent && parent !== document.body) {
+ const style = getComputedStyle(parent);
+ const overflow = style.overflow + style.overflowY + style.overflowX;
+
+ if (overflowRegex.test(overflow)) {
+ return parent;
+ }
+
+ parent = parent.parentElement;
+ }
+
+ return document.scrollingElement || document.documentElement;
+}
diff --git a/frontend/src/app/features/work-packages/components/wp-fast-table/wp-fast-table.ts b/frontend/src/app/features/work-packages/components/wp-fast-table/wp-fast-table.ts
index 6db045e9e948..f2060d82f8ce 100644
--- a/frontend/src/app/features/work-packages/components/wp-fast-table/wp-fast-table.ts
+++ b/frontend/src/app/features/work-packages/components/wp-fast-table/wp-fast-table.ts
@@ -55,7 +55,7 @@ export class WorkPackageTable {
public readonly injector:Injector,
public tableAndTimelineContainer:HTMLElement,
public scrollContainer:HTMLElement,
- public tbody:HTMLElement,
+ public tbody:HTMLTableSectionElement,
public timelineBody:HTMLElement,
public timelineController:WorkPackageTimelineTableController,
public configuration:WorkPackageTableConfiguration,
diff --git a/frontend/src/app/features/work-packages/components/wp-inline-create/inline-create-row-builder.ts b/frontend/src/app/features/work-packages/components/wp-inline-create/inline-create-row-builder.ts
index 58e0a321176e..dc479bd3e9ad 100644
--- a/frontend/src/app/features/work-packages/components/wp-inline-create/inline-create-row-builder.ts
+++ b/frontend/src/app/features/work-packages/components/wp-inline-create/inline-create-row-builder.ts
@@ -41,7 +41,7 @@ export class InlineCreateRowBuilder extends SingleRowBuilder {
};
}
- public buildCell(workPackage:WorkPackageResource, column:QueryColumn):HTMLElement|null {
+ public buildCell(workPackage:WorkPackageResource, column:QueryColumn):HTMLTableCellElement|null {
switch (column.id) {
case internalContextMenuColumn.id:
return this.buildCancelButton();
diff --git a/frontend/src/app/features/work-packages/components/wp-inline-create/wp-inline-create.component.ts b/frontend/src/app/features/work-packages/components/wp-inline-create/wp-inline-create.component.ts
index 044f12183e8f..ecab383fb156 100644
--- a/frontend/src/app/features/work-packages/components/wp-inline-create/wp-inline-create.component.ts
+++ b/frontend/src/app/features/work-packages/components/wp-inline-create/wp-inline-create.component.ts
@@ -68,6 +68,7 @@ import { onClickOrEnter } from '../wp-fast-table/handlers/click-or-enter-handler
import {
HalResourceEditingService,
} from 'core-app/shared/components/fields/edit/services/hal-resource-editing.service';
+import { delegate, DelegateEvent } from '@knowledgecode/delegate';
@Component({
selector: '[wpInlineCreate]',
@@ -101,7 +102,7 @@ export class WorkPackageInlineCreateComponent extends UntilDestroyedMixin implem
private editingSubscription:Subscription|undefined;
- private $element:JQuery;
+ private element:HTMLElement;
get isActive():boolean {
return this.mode !== 'inactive';
@@ -123,7 +124,7 @@ export class WorkPackageInlineCreateComponent extends UntilDestroyedMixin implem
}
ngOnInit() {
- this.$element = jQuery(this.elementRef.nativeElement);
+ this.element = this.elementRef.nativeElement;
}
ngAfterViewInit():void {
@@ -156,14 +157,18 @@ export class WorkPackageInlineCreateComponent extends UntilDestroyedMixin implem
* which is dynamically inserted into the action row by the inline create renderer.
*/
private registerCancelHandler() {
- this.$element.on('click keydown', `.${inlineCreateCancelClassName}`, (evt:JQuery.TriggeredEvent) => {
- onClickOrEnter(evt, () => {
+ const handler = (evt:DelegateEvent) => {
+ onClickOrEnter(evt.originalEvent, () => {
this.resetRow();
});
evt.stopImmediatePropagation();
return false;
- });
+ };
+
+ delegate(this.element)
+ .on('click', `.${inlineCreateCancelClassName}`, handler)
+ .on('keydown', `.${inlineCreateCancelClassName}`, handler);
}
/**
@@ -272,9 +277,9 @@ export class WorkPackageInlineCreateComponent extends UntilDestroyedMixin implem
private refreshRow() {
const builder = new InlineCreateRowBuilder(this.injector, this.table);
- const rowElement = this.$element.find(`.${inlineCreateRowClassName}`);
+ const rowElement = this.element.querySelector(`.${inlineCreateRowClassName}`);
- if (rowElement.length && this.currentWorkPackage) {
+ if (rowElement && this.currentWorkPackage) {
builder.refreshRow(this.currentWorkPackage, rowElement);
}
}
@@ -291,7 +296,7 @@ export class WorkPackageInlineCreateComponent extends UntilDestroyedMixin implem
const form = this.table.editing.startEditing(wp, builder.classIdentifier(wp));
const [row] = builder.buildNew(wp, form);
- this.$element.append(row);
+ this.element.append(row);
return form;
}
@@ -313,7 +318,7 @@ export class WorkPackageInlineCreateComponent extends UntilDestroyedMixin implem
public removeWorkPackageRow() {
this.wpCreate.cancelCreation();
this.currentWorkPackage = null;
- this.$element.find('.wp-row-new').remove();
+ this.element.querySelector('.wp-row-new')?.remove();
if (this.editingSubscription) {
this.editingSubscription.unsubscribe();
}
diff --git a/frontend/src/app/features/work-packages/components/wp-relations/wp-relation-row/wp-relation-row.component.ts b/frontend/src/app/features/work-packages/components/wp-relations/wp-relation-row/wp-relation-row.component.ts
index 0b4b42825638..192d43cbcf7f 100644
--- a/frontend/src/app/features/work-packages/components/wp-relations/wp-relation-row/wp-relation-row.component.ts
+++ b/frontend/src/app/features/work-packages/components/wp-relations/wp-relation-row/wp-relation-row.component.ts
@@ -25,7 +25,7 @@ export class WorkPackageRelationRowComponent extends UntilDestroyedMixin impleme
@Input() public groupByWorkPackageType:boolean;
- @ViewChild('relationDescriptionTextarea') readonly relationDescriptionTextarea:ElementRef;
+ @ViewChild('relationDescriptionTextarea') readonly relationDescriptionTextarea:ElementRef;
public relationType:string;
@@ -112,18 +112,18 @@ export class WorkPackageRelationRowComponent extends UntilDestroyedMixin impleme
public startDescriptionEdit() {
this.userInputs.showDescriptionEditForm = true;
setTimeout(() => {
- const textarea = jQuery(this.relationDescriptionTextarea.nativeElement);
- const textlen = (textarea.val() as string).length;
+ const textarea = this.relationDescriptionTextarea.nativeElement;
+ const textlen = textarea.value.length;
// Focus and set cursor to end
textarea.focus();
- textarea.prop('selectionStart', textlen);
- textarea.prop('selectionEnd', textlen);
+ textarea.selectionStart = textlen;
+ textarea.selectionEnd = textlen;
});
}
- public handleDescriptionKey($event:JQuery.TriggeredEvent) {
- if ($event.key === 'Escape') {
+ public handleDescriptionKey(event:KeyboardEvent) {
+ if (event.key === 'Escape') {
this.cancelDescriptionEdit();
}
}
@@ -157,7 +157,7 @@ export class WorkPackageRelationRowComponent extends UntilDestroyedMixin impleme
}
}
- public cancelRelationTypeEditOnEscape(evt:JQuery.TriggeredEvent) {
+ public cancelRelationTypeEditOnEscape(evt:KeyboardEvent) {
this.userInputs.showRelationTypesForm = false;
}
diff --git a/frontend/src/app/features/work-packages/components/wp-relations/wp-relations-create/wp-relations-autocomplete/wp-relations-autocomplete.component.ts b/frontend/src/app/features/work-packages/components/wp-relations/wp-relations-create/wp-relations-autocomplete/wp-relations-autocomplete.component.ts
index c12cc223530e..5e79e5fb615d 100644
--- a/frontend/src/app/features/work-packages/components/wp-relations/wp-relations-create/wp-relations-autocomplete/wp-relations-autocomplete.component.ts
+++ b/frontend/src/app/features/work-packages/components/wp-relations/wp-relations-create/wp-relations-autocomplete/wp-relations-autocomplete.component.ts
@@ -97,9 +97,9 @@ export class WorkPackageRelationsAutocompleteComponent extends OpAutocompleterCo
this.ngZone.runOutsideAngular(() => {
setTimeout(() => {
this.ngSelectInstance.dropdownPanel.adjustPosition();
- jQuery(this.hiddenOverflowContainer).one('scroll', () => {
+ document.querySelector(this.hiddenOverflowContainer)?.addEventListener('scroll', () => {
this.ngSelectInstance.close();
- });
+ }, { once: true });
}, 25);
});
}
diff --git a/frontend/src/app/features/work-packages/components/wp-single-view-tabs/watchers-tab/watchers-tab.component.ts b/frontend/src/app/features/work-packages/components/wp-single-view-tabs/watchers-tab/watchers-tab.component.ts
index 967429117693..6e785b501bb1 100644
--- a/frontend/src/app/features/work-packages/components/wp-single-view-tabs/watchers-tab/watchers-tab.component.ts
+++ b/frontend/src/app/features/work-packages/components/wp-single-view-tabs/watchers-tab/watchers-tab.component.ts
@@ -66,7 +66,7 @@ export class WorkPackageWatchersTabComponent extends UntilDestroyedMixin impleme
public availableWatchersPath:string;
- private $element:JQuery;
+ private element:HTMLElement;
public watching:any[] = [];
@@ -94,8 +94,7 @@ export class WorkPackageWatchersTabComponent extends UntilDestroyedMixin impleme
}
public ngOnInit() {
- this.$element = jQuery(this.elementRef.nativeElement);
-
+ this.element = this.elementRef.nativeElement;
const { workPackageId } = this.uiRouterGlobals.params as unknown as { workPackageId:string };
this.workPackageId = (this.workPackage.id as string) || workPackageId;
diff --git a/frontend/src/app/features/work-packages/components/wp-single-view/wp-single-view.component.ts b/frontend/src/app/features/work-packages/components/wp-single-view/wp-single-view.component.ts
index 6e2a145dda87..fd521271b3f6 100644
--- a/frontend/src/app/features/work-packages/components/wp-single-view/wp-single-view.component.ts
+++ b/frontend/src/app/features/work-packages/components/wp-single-view/wp-single-view.component.ts
@@ -140,7 +140,7 @@ export class WorkPackageSingleViewComponent extends UntilDestroyedMixin implemen
public uiSelfRef:string;
- $element:JQuery;
+ element:HTMLElement;
projectStorages = new BehaviorSubject([]);
@@ -166,7 +166,7 @@ export class WorkPackageSingleViewComponent extends UntilDestroyedMixin implemen
}
public ngOnInit():void {
- this.$element = jQuery(this.elementRef.nativeElement as HTMLElement);
+ this.element = this.elementRef.nativeElement as HTMLElement;
this.isNewResource = isNewResource(this.workPackage);
@@ -309,7 +309,7 @@ export class WorkPackageSingleViewComponent extends UntilDestroyedMixin implemen
}
showTwoColumnLayout():boolean {
- return this.$element[0].getBoundingClientRect().width > 750;
+ return this.element.getBoundingClientRect().width > 750;
}
private rebuildGroupedFields(change:WorkPackageChangeset, attributeGroups:any) {
@@ -431,9 +431,9 @@ export class WorkPackageSingleViewComponent extends UntilDestroyedMixin implemen
}
private getAttributesGroupId(group:any):string {
- const overflowingIdentifier = this.$element
- .find(`[data-group-name=\'${group.name}\']`)
- .data(overflowingContainerAttribute);
+ const overflowingIdentifier = this.element
+ .querySelector(`[data-group-name=\'${group.name}\']`)
+ ?.dataset[overflowingContainerAttribute];
if (overflowingIdentifier) {
return overflowingIdentifier.replace('.__overflowing_', '');
diff --git a/frontend/src/app/features/work-packages/components/wp-table/configuration-modal/wp-table-configuration.modal.ts b/frontend/src/app/features/work-packages/components/wp-table/configuration-modal/wp-table-configuration.modal.ts
index 4457333ed698..2b9f10d1393b 100644
--- a/frontend/src/app/features/work-packages/components/wp-table/configuration-modal/wp-table-configuration.modal.ts
+++ b/frontend/src/app/features/work-packages/components/wp-table/configuration-modal/wp-table-configuration.modal.ts
@@ -93,7 +93,7 @@ export class WpTableConfigurationModalComponent extends OpModalComponent impleme
}
ngOnInit() {
- this.$element = this.elementRef.nativeElement as HTMLElement;
+ this.element = this.elementRef.nativeElement as HTMLElement;
this.tabPortalHost = new TabPortalOutlet(
this.wpTableConfigurationService.tabs,
@@ -149,7 +149,7 @@ export class WpTableConfigurationModalComponent extends OpModalComponent impleme
}
protected get afterFocusOn():HTMLElement {
- return this.$element;
+ return this.element;
}
protected loadForm() {
diff --git a/frontend/src/app/features/work-packages/components/wp-table/sort-header/sort-header.directive.ts b/frontend/src/app/features/work-packages/components/wp-table/sort-header/sort-header.directive.ts
index db81a0741df0..b18e3ba573bb 100644
--- a/frontend/src/app/features/work-packages/components/wp-table/sort-header/sort-header.directive.ts
+++ b/frontend/src/app/features/work-packages/components/wp-table/sort-header/sort-header.directive.ts
@@ -203,7 +203,7 @@ export class SortHeaderDirective extends UntilDestroyedMixin implements AfterVie
return this.table && this.table.configuration.hierarchyToggleEnabled;
}
- toggleHierarchy(evt:JQuery.TriggeredEvent) {
+ toggleHierarchy(evt:Event) {
if (this.wpTableHierarchies.toggleState()) {
this.wpTableGroupBy.disable();
}
diff --git a/frontend/src/app/features/work-packages/components/wp-table/table-actions/actions/unlink-table-action.ts b/frontend/src/app/features/work-packages/components/wp-table/table-actions/actions/unlink-table-action.ts
index 9277c8472af0..8b2a4003f09d 100644
--- a/frontend/src/app/features/work-packages/components/wp-table/table-actions/actions/unlink-table-action.ts
+++ b/frontend/src/app/features/work-packages/components/wp-table/table-actions/actions/unlink-table-action.ts
@@ -47,7 +47,7 @@ export class OpUnlinkTableAction extends OpTableAction {
element.classList.add(contextColumnIcon, 'wp-table-action--unlink');
element.dataset.workPackageId = this.workPackage.id!;
element.appendChild(opIconElement('icon', 'icon-close'));
- jQuery(element).click((event) => {
+ element.addEventListener('click', (event) => {
event.preventDefault();
this.onClick(this.workPackage);
});
diff --git a/frontend/src/app/features/work-packages/components/wp-table/table-pagination/wp-table-pagination.component.spec.ts b/frontend/src/app/features/work-packages/components/wp-table/table-pagination/wp-table-pagination.component.spec.ts
index 0703ed58d0ce..c15cc1b2da6d 100644
--- a/frontend/src/app/features/work-packages/components/wp-table/table-pagination/wp-table-pagination.component.spec.ts
+++ b/frontend/src/app/features/work-packages/components/wp-table/table-pagination/wp-table-pagination.component.spec.ts
@@ -62,8 +62,8 @@ function setupMocks(paginationService:PaginationService) {
spyOn(paginationService, 'getPaginationOptions').and.callFake(() => options);
}
-function pageString(element:JQuery) {
- return element.find('.op-pagination--range').text().trim();
+function pageString(element:HTMLElement) {
+ return element.querySelector('.op-pagination--range')?.textContent?.trim() || '';
}
describe('wpTablePagination Directive', () => {
@@ -103,7 +103,7 @@ describe('wpTablePagination Directive', () => {
setupMocks(paginationService);
const fixture = TestBed.createComponent(WorkPackageTablePaginationComponent);
const app:WorkPackageTablePaginationComponent = fixture.debugElement.componentInstance;
- const element = jQuery(fixture.elementRef.nativeElement);
+ const element = fixture.elementRef.nativeElement;
app.pagination = new PaginationInstance(1, 0, 10);
app.update();
@@ -122,14 +122,15 @@ describe('wpTablePagination Directive', () => {
setupMocks(paginationService);
const fixture = TestBed.createComponent(WorkPackageTablePaginationComponent);
const app:WorkPackageTablePaginationComponent = fixture.debugElement.componentInstance;
- const element = jQuery(fixture.elementRef.nativeElement);
+ const element = fixture.elementRef.nativeElement;
app.pagination = new PaginationInstance(2, 11, 10);
app.update();
fixture.detectChanges();
- const liWithNextLink = element.find('.op-pagination--item-link_next').parent('li');
- const attrHidden = liWithNextLink.attr('hidden');
+ const liWithNextLink = element.querySelector('.op-pagination--item-link_next')?.parentElement;
+ expect(liWithNextLink?.matches('li')).toBeTrue();
+ const attrHidden = liWithNextLink.getAttribute('hidden');
expect(attrHidden).toBeDefined();
}));
});
@@ -139,10 +140,10 @@ describe('wpTablePagination Directive', () => {
setupMocks(paginationService);
const fixture = TestBed.createComponent(WorkPackageTablePaginationComponent);
const app:WorkPackageTablePaginationComponent = fixture.debugElement.componentInstance;
- const element = jQuery(fixture.elementRef.nativeElement);
+ const element = fixture.elementRef.nativeElement;
function numberOfPageNumberLinks() {
- return element.find('button[data-rel="next"]').length;
+ return element.querySelectorAll('button[data-rel="next"]').length;
}
app.pagination = new PaginationInstance(1, 1, 10);
diff --git a/frontend/src/app/features/work-packages/components/wp-table/timeline/cells/timeline-cell-renderer.ts b/frontend/src/app/features/work-packages/components/wp-table/timeline/cells/timeline-cell-renderer.ts
index 2880b5e2ae00..58b06bca585f 100644
--- a/frontend/src/app/features/work-packages/components/wp-table/timeline/cells/timeline-cell-renderer.ts
+++ b/frontend/src/app/features/work-packages/components/wp-table/timeline/cells/timeline-cell-renderer.ts
@@ -187,8 +187,10 @@ export class TimelineCellRenderer {
const projection = renderInfo.change.projectedResource;
let direction:Exclude;
+ const target = ev.target as HTMLElement;
+
// Update the cursor and maybe set start/due values
- if (jQuery(ev.target!).hasClass(classNameLeftHandle)) {
+ if (target.classList.contains(classNameLeftHandle)) {
// only left
direction = 'left';
this.mouseDirection = 'left';
@@ -196,7 +198,7 @@ export class TimelineCellRenderer {
if (projection.startDate === null) {
projection.startDate = projection.dueDate;
}
- } else if (jQuery(ev.target!).hasClass(classNameRightHandle) || dateForCreate) {
+ } else if (target.classList.contains(classNameRightHandle) || dateForCreate) {
// only right
direction = 'right';
this.mouseDirection = 'right';
@@ -286,7 +288,7 @@ export class TimelineCellRenderer {
element.style.backgroundImage = ''; // required! unable to disable "fade out bar" with css
if (renderInfo.viewParams.selectionModeStart === `${renderInfo.workPackage.id!}`) {
- jQuery(element).addClass(timelineMarkerSelectionStartClass);
+ element.classList.add(timelineMarkerSelectionStartClass);
element.style.background = 'none';
}
}
diff --git a/frontend/src/app/features/work-packages/components/wp-table/timeline/cells/timeline-milestone-cell-renderer.ts b/frontend/src/app/features/work-packages/components/wp-table/timeline/cells/timeline-milestone-cell-renderer.ts
index 7030ac4d72e1..224ecb58743b 100644
--- a/frontend/src/app/features/work-packages/components/wp-table/timeline/cells/timeline-milestone-cell-renderer.ts
+++ b/frontend/src/app/features/work-packages/components/wp-table/timeline/cells/timeline-milestone-cell-renderer.ts
@@ -124,7 +124,7 @@ export class TimelineMilestoneCellRenderer extends TimelineCellRenderer {
return false;
}
- const diamond = jQuery('.diamond', element)[0];
+ const diamond = element.querySelector('.diamond')!;
diamond.style.width = `${15}px`;
diamond.style.height = `${15}px`;
diff --git a/frontend/src/app/features/work-packages/components/wp-table/timeline/cells/wp-timeline-cell-mouse-handler.ts b/frontend/src/app/features/work-packages/components/wp-table/timeline/cells/wp-timeline-cell-mouse-handler.ts
index b976b2e73a4c..4f660b8e60dd 100644
--- a/frontend/src/app/features/work-packages/components/wp-table/timeline/cells/wp-timeline-cell-mouse-handler.ts
+++ b/frontend/src/app/features/work-packages/components/wp-table/timeline/cells/wp-timeline-cell-mouse-handler.ts
@@ -45,6 +45,7 @@ import {
} from './timeline-cell-renderer';
import { RenderInfo } from '../wp-timeline';
import { WorkPackageTimelineTableController } from '../container/wp-timeline-container.directive';
+import { target } from 'core-app/shared/helpers/event-helpers';
export function registerWorkPackageMouseHandler(this:void,
injector:Injector,
@@ -63,7 +64,7 @@ export function registerWorkPackageMouseHandler(this:void,
renderInfo.change = halEditing.changeFor(renderInfo.workPackage);
let placeholderForEmptyCell:HTMLElement;
- const jBody = jQuery('body');
+ const bodyTarget = target(document.body);
// handles change to existing work packages
bar.onmousedown = (ev:MouseEvent) => {
@@ -94,7 +95,7 @@ export function registerWorkPackageMouseHandler(this:void,
// add/remove css class while drag'n'drop is active
const classNameActiveDrag = 'active-drag';
bar.classList.add(classNameActiveDrag);
- jBody.on('mouseup.timelinecell', () => bar.classList.remove(classNameActiveDrag));
+ bodyTarget.on('mouseup.timelinecell', () => bar.classList.remove(classNameActiveDrag));
workPackageTimeline.disableViewParamsCalculation = true;
mouseDownStartDay = getCursorOffsetInDaysFromLeft(ev);
@@ -109,14 +110,14 @@ export function registerWorkPackageMouseHandler(this:void,
// Determine what attributes of the work package should be changed
const direction = renderer.onMouseDown(ev, null, renderInfo, labels);
- jBody.on('mousemove.timelinecell', createMouseMoveFn(direction));
- jBody.on('keyup.timelinecell', keyPressFn);
- jBody.on('mouseup.timelinecell', () => deactivate(direction, false));
+ bodyTarget.on('mousemove.timelinecell', createMouseMoveFn(direction));
+ bodyTarget.on('keyup.timelinecell', keyPressFn);
+ bodyTarget.on('mouseup.timelinecell', () => deactivate(direction, false));
}
function createMouseMoveFn(direction:MouseDirection) {
- return (ev:JQuery.MouseMoveEvent) => {
- const days = getCursorOffsetInDaysFromLeft(ev.originalEvent as MouseEvent) - (mouseDownStartDay as number);
+ return (ev:MouseEvent) => {
+ const days = getCursorOffsetInDaysFromLeft(ev) - (mouseDownStartDay as number);
const offsetDayCurrent = Math.floor(ev.offsetX / renderInfo.viewParams.pixelPerDay);
const dayUnderCursor = renderInfo.viewParams.dateDisplayStart.clone().add(offsetDayCurrent, 'days');
@@ -124,8 +125,7 @@ export function registerWorkPackageMouseHandler(this:void,
};
}
- function keyPressFn(ev:JQuery.TriggeredEvent) {
- const kev:KeyboardEvent = ev.originalEvent as KeyboardEvent;
+ function keyPressFn(kev:KeyboardEvent) {
if (kev.key === 'Escape') {
deactivate(null, true);
}
@@ -182,19 +182,19 @@ export function registerWorkPackageMouseHandler(this:void,
return;
}
- jBody.on('mousemove.emptytimelinecell', mouseMoveOnEmptyCellFn(offsetDayStart, direction));
- jBody.on('mouseup.emptytimelinecell', () => deactivate(direction, false));
+ bodyTarget.on('mousemove.emptytimelinecell', mouseMoveOnEmptyCellFn(offsetDayStart, direction));
+ bodyTarget.on('mouseup.emptytimelinecell', () => deactivate(direction, false));
cell.onmouseup = () => {
deactivate(direction, false);
};
- jBody.on('keyup.timelinecell', keyPressFn);
+ bodyTarget.on('keyup.timelinecell', keyPressFn);
};
}
function mouseMoveOnEmptyCellFn(offsetDayStart:number, mouseDownType:MouseDirection) {
- return (ev:JQuery.MouseMoveEvent) => {
+ return (ev:MouseEvent) => {
placeholderForEmptyCell.remove();
const relativePosition = Math.abs(cell.getBoundingClientRect().x - ev.clientX);
const offsetDayCurrent = Math.floor(relativePosition / renderInfo.viewParams.pixelPerDay);
@@ -216,8 +216,8 @@ export function registerWorkPackageMouseHandler(this:void,
bar.style.pointerEvents = 'auto';
- jBody.off('.timelinecell');
- jBody.off('.emptytimelinecell');
+ bodyTarget.off('.timelinecell');
+ bodyTarget.off('.emptytimelinecell');
workPackageTimeline.resetCursor();
mouseDownStartDay = null;
diff --git a/frontend/src/app/features/work-packages/components/wp-table/timeline/cells/wp-timeline-cell.ts b/frontend/src/app/features/work-packages/components/wp-table/timeline/cells/wp-timeline-cell.ts
index 92fc0420511e..584a6be0a772 100644
--- a/frontend/src/app/features/work-packages/components/wp-table/timeline/cells/wp-timeline-cell.ts
+++ b/frontend/src/app/features/work-packages/components/wp-table/timeline/cells/wp-timeline-cell.ts
@@ -99,7 +99,10 @@ export class WorkPackageTimelineCell {
}
public clear() {
- this.cellElement.html('');
+ if (this.cellElement) {
+ this.cellElement.innerHTML = '';
+ }
+
this.wpElement = null;
}
@@ -107,15 +110,15 @@ export class WorkPackageTimelineCell {
return this.workPackageTimeline.timelineBody;
}
- private get cellElement():JQuery {
- return this.cellContainer.find(`.${this.classIdentifier}`);
+ private get cellElement() {
+ return this.cellContainer.querySelector(`.${this.classIdentifier}`);
}
private lazyInit(renderer:TimelineCellRenderer, renderInfo:RenderInfo):Promise {
- const body = this.workPackageTimeline.timelineBody[0];
+ const body = this.workPackageTimeline.timelineBody;
const cell = this.cellElement;
- if (!cell.length) {
+ if (!cell) {
return Promise.reject('uninitialized');
}
@@ -151,7 +154,7 @@ export class WorkPackageTimelineCell {
this.halEvents,
this.notificationService,
this.loadingIndicator,
- cell[0],
+ cell,
this.wpElement,
this.labels,
renderer,
diff --git a/frontend/src/app/features/work-packages/components/wp-table/timeline/container/wp-timeline-container.directive.ts b/frontend/src/app/features/work-packages/components/wp-table/timeline/container/wp-timeline-container.directive.ts
index e1f9132cc7f4..4efb83ff7447 100644
--- a/frontend/src/app/features/work-packages/components/wp-table/timeline/container/wp-timeline-container.directive.ts
+++ b/frontend/src/app/features/work-packages/components/wp-table/timeline/container/wp-timeline-container.directive.ts
@@ -96,7 +96,7 @@ import { IDay } from 'core-app/core/state/days/day.model';
standalone: false,
})
export class WorkPackageTimelineTableController extends UntilDestroyedMixin implements AfterViewInit {
- private $element:JQuery;
+ private element:HTMLElement;
public workPackageTable:WorkPackageTable;
@@ -110,9 +110,9 @@ export class WorkPackageTimelineTableController extends UntilDestroyedMixin impl
private cellsRenderer = new WorkPackageTimelineCellsRenderer(this.injector, this);
- public outerContainer:JQuery;
+ public outerContainer:HTMLElement;
- public timelineBody:JQuery;
+ public timelineBody:HTMLElement;
private selectionParams:{ notification:IToast|null } = {
notification: null,
@@ -165,7 +165,7 @@ export class WorkPackageTimelineTableController extends UntilDestroyedMixin impl
}
ngAfterViewInit() {
- this.$element = jQuery(this.elementRef.nativeElement);
+ this.element = this.elementRef.nativeElement;
const scrollBar = document.querySelector('.work-packages-tabletimeline--timeline-side');
if (scrollBar) {
@@ -179,11 +179,11 @@ export class WorkPackageTimelineTableController extends UntilDestroyedMixin impl
};
// Get the outer container for width computation
- this.outerContainer = this.$element.find('.wp-table-timeline--outer');
- this.timelineBody = this.$element.find('.wp-table-timeline--body');
+ this.outerContainer = this.element.querySelector('.wp-table-timeline--outer')!;
+ this.timelineBody = this.element.querySelector('.wp-table-timeline--body')!;
// Register this instance to the table
- this.wpTableComponent.registerTimeline(this, this.timelineBody[0]);
+ this.wpTableComponent.registerTimeline(this, this.timelineBody);
// Refresh on window resize events
window.addEventListener('wp-resize.timeline', () => this.refreshRequest.putValue(undefined));
@@ -224,11 +224,12 @@ export class WorkPackageTimelineTableController extends UntilDestroyedMixin impl
}
getAbsoluteLeftCoordinates():number {
- return this.$element.offset()!.left;
+ const rect = this.element.getBoundingClientRect();
+ return rect.left + window.pageXOffset;
}
getParentScrollContainer() {
- return this.outerContainer.closest(selectorTimelineSide)[0];
+ return this.outerContainer.closest(selectorTimelineSide)!;
}
get viewParameters():TimelineViewParameters {
@@ -259,7 +260,7 @@ export class WorkPackageTimelineTableController extends UntilDestroyedMixin impl
timeOutput('refreshView() in timeline container', async () => {
// Reset the width of the outer container if its content shrinks
- this.outerContainer.css('width', 'auto');
+ this.outerContainer.style.setProperty('width', 'auto');
this.calculateViewParams(this._viewParameters);
@@ -278,8 +279,8 @@ export class WorkPackageTimelineTableController extends UntilDestroyedMixin impl
// Calculate overflowing width to set to outer container
// required to match width in all child divs.
// The header is the only one reliable, as it already has the final width.
- const currentWidth = this.$element.find(timelineHeaderSelector)[0].scrollWidth;
- this.outerContainer.width(currentWidth);
+ const currentWidth = this.element.querySelector(timelineHeaderSelector)!.scrollWidth;
+ this.outerContainer.style.setProperty('width', `${currentWidth}px`);
// Mark rendering event in a timeout to give DOM some time
setTimeout(() => {
@@ -335,19 +336,19 @@ export class WorkPackageTimelineTableController extends UntilDestroyedMixin impl
}
forceCursor(cursor:string) {
- jQuery(`.${timelineElementCssClass}`).css('cursor', cursor);
- jQuery('.wp-timeline-cell').css('cursor', cursor);
- jQuery('.hascontextmenu').css('cursor', cursor);
- jQuery('.leftHandle').css('cursor', cursor);
- jQuery('.rightHandle').css('cursor', cursor);
+ document.querySelectorAll(`.${timelineElementCssClass}`).forEach((elem) => elem.style.cursor = cursor);
+ document.querySelectorAll('.wp-timeline-cell').forEach((elem) => elem.style.cursor = cursor);
+ document.querySelectorAll('.hascontextmenu').forEach((elem) => elem.style.cursor = cursor);
+ document.querySelectorAll('.leftHandle').forEach((elem) => elem.style.cursor = cursor);
+ document.querySelectorAll('.rightHandle').forEach((elem) => elem.style.cursor = cursor);
}
resetCursor() {
- jQuery(`.${timelineElementCssClass}`).css('cursor', '');
- jQuery('.wp-timeline-cell').css('cursor', '');
- jQuery('.hascontextmenu').css('cursor', '');
- jQuery('.leftHandle').css('cursor', '');
- jQuery('.rightHandle').css('cursor', '');
+ document.querySelectorAll(`.${timelineElementCssClass}`).forEach((elem) => elem.style.cursor = '');
+ document.querySelectorAll('.wp-timeline-cell').forEach((elem) => elem.style.cursor = '');
+ document.querySelectorAll('.hascontextmenu').forEach((elem) => elem.style.cursor = '');
+ document.querySelectorAll('.leftHandle').forEach((elem) => elem.style.cursor = '');
+ document.querySelectorAll('.rightHandle').forEach((elem) => elem.style.cursor = '');
}
private resetSelectionMode() {
@@ -360,8 +361,8 @@ export class WorkPackageTimelineTableController extends UntilDestroyedMixin impl
Mousetrap.unbind('esc');
- this.$element.removeClass('active-selection-mode');
- jQuery(`.${timelineMarkerSelectionStartClass}`).removeClass(timelineMarkerSelectionStartClass);
+ this.element.classList.remove('active-selection-mode');
+ document.querySelector(`.${timelineMarkerSelectionStartClass}`)?.classList.remove(timelineMarkerSelectionStartClass);
this.refreshView();
}
@@ -377,7 +378,7 @@ export class WorkPackageTimelineTableController extends UntilDestroyedMixin impl
Mousetrap.bind('esc', () => this.resetSelectionMode());
this.selectionParams.notification = this.toastService.addNotice(this.text.selectionMode);
- this.$element.addClass('active-selection-mode');
+ this.element.classList.add('active-selection-mode');
this.refreshView();
}
@@ -447,8 +448,7 @@ export class WorkPackageTimelineTableController extends UntilDestroyedMixin impl
// RR: kept both variants for documentation purpose.
// A: calculate the minimal width based on the width of the timeline view
// B: calculate the minimal width based on the window width
- const width = this.$element.children().width()!; // A
- // const width = jQuery('body').width(); // B
+ const width = this.element.children[0]?.clientWidth || document.body.clientWidth; // A with fallback to B
const { pixelPerDay } = currentParams;
const visibleDays = Math.ceil((width / pixelPerDay) * 1.5);
@@ -484,7 +484,7 @@ export class WorkPackageTimelineTableController extends UntilDestroyedMixin impl
const workPackagesToCalculateWidthFrom = this.getWorkPackagesToCalculateTimelineWidthFrom();
const daysSpan = calculateDaySpan(workPackagesToCalculateWidthFrom, this.states.workPackages, this._viewParameters);
- const timelineWidthInPx = this.$element.parent().width()! - (2 * requiredPixelMarginLeft);
+ const timelineWidthInPx = this.element.parentElement?.clientWidth! - (2 * requiredPixelMarginLeft);
for (const zoomLevel of zoomLevelOrder) {
const pixelPerDay = getPixelPerDayForZoomLevel(zoomLevel);
diff --git a/frontend/src/app/features/work-packages/components/wp-table/timeline/global-elements/wp-timeline-relations.directive.ts b/frontend/src/app/features/work-packages/components/wp-table/timeline/global-elements/wp-timeline-relations.directive.ts
index 45ec80484fcc..5441d6c24e82 100644
--- a/frontend/src/app/features/work-packages/components/wp-table/timeline/global-elements/wp-timeline-relations.directive.ts
+++ b/frontend/src/app/features/work-packages/components/wp-table/timeline/global-elements/wp-timeline-relations.directive.ts
@@ -85,7 +85,7 @@ function newSegment(vp:TimelineViewParameters,
export class WorkPackageTableTimelineRelations extends UntilDestroyedMixin implements OnInit {
@InjectField() querySpace:IsolatedQuerySpace;
- private container:JQuery;
+ private container:HTMLElement;
private workPackagesWithRelations:{ [workPackageId:string]:RelationsStateValue } = {};
@@ -99,8 +99,8 @@ export class WorkPackageTableTimelineRelations extends UntilDestroyedMixin imple
}
ngOnInit() {
- const $element = jQuery(this.elementRef.nativeElement);
- this.container = $element.find('.wp-table-timeline--relations');
+ const element = this.elementRef.nativeElement;
+ this.container = element.querySelector('.wp-table-timeline--relations');
this.workPackageTimelineTableController
.onRefreshRequested('relations', (vp:TimelineViewParameters) => this.refreshView());
@@ -188,12 +188,12 @@ export class WorkPackageTableTimelineRelations extends UntilDestroyedMixin imple
private removeRelationElementsForWorkPackage(workPackageId:string) {
const className = workPackagePrefix(workPackageId);
- const found = this.container.find(`.${className}`);
- found.remove();
+ const found = this.container.querySelectorAll(`.${className}`);
+ found.forEach((elem) => elem.remove());
}
private removeAllVisibleElements() {
- this.container.find(`.${timelineGlobalElementCssClassname}`).remove();
+ this.container.querySelectorAll(`.${timelineGlobalElementCssClassname}`).forEach((elem) => elem.remove());
}
private renderElements() {
diff --git a/frontend/src/app/features/work-packages/components/wp-table/timeline/global-elements/wp-timeline-static-elements.directive.ts b/frontend/src/app/features/work-packages/components/wp-table/timeline/global-elements/wp-timeline-static-elements.directive.ts
index 04fc2a3356b1..38db181e62ad 100644
--- a/frontend/src/app/features/work-packages/components/wp-table/timeline/global-elements/wp-timeline-static-elements.directive.ts
+++ b/frontend/src/app/features/work-packages/components/wp-table/timeline/global-elements/wp-timeline-static-elements.directive.ts
@@ -45,7 +45,7 @@ import { TodayLineElement } from './wp-timeline.today-line';
standalone: false,
})
export class WorkPackageTableTimelineStaticElements implements OnInit {
- public $element:HTMLElement;
+ public element:HTMLElement;
private container:HTMLElement;
@@ -54,7 +54,7 @@ export class WorkPackageTableTimelineStaticElements implements OnInit {
constructor(elementRef:ElementRef,
public states:States,
public workPackageTimelineTableController:WorkPackageTimelineTableController) {
- this.$element = elementRef.nativeElement;
+ this.element = elementRef.nativeElement;
this.elements = [
new TodayLineElement(),
@@ -62,7 +62,7 @@ export class WorkPackageTableTimelineStaticElements implements OnInit {
}
ngOnInit() {
- this.container = this.$element.querySelector('.wp-table-timeline--static-elements') as HTMLElement;
+ this.container = this.element.querySelector('.wp-table-timeline--static-elements') as HTMLElement;
this.workPackageTimelineTableController
.onRefreshRequested('static elements', (vp:TimelineViewParameters) => this.update(vp));
}
diff --git a/frontend/src/app/features/work-packages/components/wp-table/timeline/grid/wp-timeline-grid.directive.ts b/frontend/src/app/features/work-packages/components/wp-table/timeline/grid/wp-timeline-grid.directive.ts
index 4a9dd2dfdbba..4b982d1c73aa 100644
--- a/frontend/src/app/features/work-packages/components/wp-table/timeline/grid/wp-timeline-grid.directive.ts
+++ b/frontend/src/app/features/work-packages/components/wp-table/timeline/grid/wp-timeline-grid.directive.ts
@@ -50,7 +50,7 @@ import { WeekdayService } from 'core-app/core/days/weekday.service';
export class WorkPackageTableTimelineGrid implements AfterViewInit {
private activeZoomLevel:TimelineZoomLevel;
- private gridContainer:JQuery;
+ private gridContainer:HTMLElement;
constructor(
private elementRef:ElementRef,
@@ -59,8 +59,8 @@ export class WorkPackageTableTimelineGrid implements AfterViewInit {
) {}
ngAfterViewInit():void {
- const $element = jQuery(this.elementRef.nativeElement);
- this.gridContainer = $element.find('.wp-table-timeline--grid');
+ const element = this.elementRef.nativeElement;
+ this.gridContainer = element.querySelector('.wp-table-timeline--grid');
this.wpTimeline.onRefreshRequested('grid', (vp:TimelineViewParameters) => this.refreshView(vp));
}
@@ -69,7 +69,7 @@ export class WorkPackageTableTimelineGrid implements AfterViewInit {
}
private renderLabels(vp:TimelineViewParameters):void {
- this.gridContainer.empty();
+ this.gridContainer.innerHTML = '';
switch (vp.settings.zoomLevel) {
case 'days':
@@ -170,7 +170,7 @@ export class WorkPackageTableTimelineGrid implements AfterViewInit {
cell.classList.add(timelineElementCssClass, timelineGridElementCssClass);
cell.style.left = calculatePositionValueForDayCount(vp, start.diff(startView, 'days'));
cell.style.width = calculatePositionValueForDayCount(vp, end.diff(start, 'days') + 1);
- this.gridContainer[0].appendChild(cell);
+ this.gridContainer.appendChild(cell);
cellCallback(start, cell);
}
setTimeout(() => {
@@ -179,7 +179,7 @@ export class WorkPackageTableTimelineGrid implements AfterViewInit {
cell.classList.add(timelineElementCssClass, timelineGridElementCssClass);
cell.style.left = calculatePositionValueForDayCount(vp, start.diff(startView, 'days'));
cell.style.width = calculatePositionValueForDayCount(vp, end.diff(start, 'days') + 1);
- this.gridContainer[0].appendChild(cell);
+ this.gridContainer.appendChild(cell);
cellCallback(start, cell);
}
}, 0);
diff --git a/frontend/src/app/features/work-packages/components/wp-table/timeline/header/wp-timeline-header.directive.ts b/frontend/src/app/features/work-packages/components/wp-table/timeline/header/wp-timeline-header.directive.ts
index d05b36f73ea0..f7d40eb6c3f4 100644
--- a/frontend/src/app/features/work-packages/components/wp-table/timeline/header/wp-timeline-header.directive.ts
+++ b/frontend/src/app/features/work-packages/components/wp-table/timeline/header/wp-timeline-header.directive.ts
@@ -46,17 +46,17 @@ import {
standalone: false,
})
export class WorkPackageTimelineHeaderController implements OnInit {
- public $element:JQuery;
+ public element:HTMLElement;
private activeZoomLevel:TimelineZoomLevel;
- private innerHeader:JQuery;
+ private innerHeader:HTMLElement;
constructor(elementRef:ElementRef,
readonly I18n:I18nService,
readonly wpTimelineService:WorkPackageViewTimelineService,
readonly workPackageTimelineTableController:WorkPackageTimelineTableController) {
- this.$element = jQuery(elementRef.nativeElement);
+ this.element = elementRef.nativeElement;
}
ngOnInit() {
@@ -65,13 +65,13 @@ export class WorkPackageTimelineHeaderController implements OnInit {
}
refreshView(vp:TimelineViewParameters) {
- this.innerHeader = this.$element.find('.wp-table-timeline--header-inner');
+ this.innerHeader = this.element.querySelector('.wp-table-timeline--header-inner')!;
this.renderLabels(vp);
}
private renderLabels(vp:TimelineViewParameters):void {
- this.innerHeader.empty();
- this.innerHeader.attr('data-current-zoom-level', this.wpTimelineService.zoomLevel);
+ this.innerHeader.innerHTML = '';
+ this.innerHeader.setAttribute('data-current-zoom-level', this.wpTimelineService.zoomLevel);
switch (vp.settings.zoomLevel) {
case 'days':
diff --git a/frontend/src/app/features/work-packages/components/wp-table/wp-table-hover-sync.ts b/frontend/src/app/features/work-packages/components/wp-table/wp-table-hover-sync.ts
index 139f5bebaa31..eff7497b1a0e 100644
--- a/frontend/src/app/features/work-packages/components/wp-table/wp-table-hover-sync.ts
+++ b/frontend/src/app/features/work-packages/components/wp-table/wp-table-hover-sync.ts
@@ -32,14 +32,14 @@ export class WpTableHoverSync {
private lastHoveredElement:Element | null = null;
private eventListener = (evt:MouseEvent) => {
- const target = evt.target as Element|null;
+ const target = evt.target as HTMLElement|null;
if (target && target !== this.lastHoveredElement) {
this.handleHover(target);
}
this.lastHoveredElement = target;
};
- constructor(private tableAndTimeline:JQuery) {
+ constructor(private tableAndTimeline:HTMLElement) {
}
activate() {
@@ -51,26 +51,17 @@ export class WpTableHoverSync {
this.removeAllHoverClasses();
}
- private locateHoveredTableRow(child:JQuery):Element | null {
- const parent = child.closest('tr');
- if (parent.length === 0) {
- return null;
- }
- return parent[0];
+ private locateHoveredTableRow(child:HTMLElement):HTMLTableRowElement | null {
+ return child.closest('tr');
}
- private locateHoveredTimelineRow(child:JQuery):Element | null {
- const parent = child.closest('div.wp-timeline-cell');
- if (parent.length === 0) {
- return null;
- }
- return parent[0];
+ private locateHoveredTimelineRow(child:HTMLElement):HTMLElement | null {
+ return child.closest('div.wp-timeline-cell');
}
- private handleHover(element:Element) {
- const $element = jQuery(element) as JQuery;
- const parentTableRow = this.locateHoveredTableRow($element);
- const parentTimelineRow = this.locateHoveredTimelineRow($element);
+ private handleHover(element:HTMLElement) {
+ const parentTableRow = this.locateHoveredTableRow(element);
+ const parentTimelineRow = this.locateHoveredTimelineRow(element);
// remove all hover classes if cursor does not hover a row
if (parentTableRow === null && parentTimelineRow === null) {
@@ -89,21 +80,21 @@ export class WpTableHoverSync {
const hovered = parentTableRow !== null ? parentTableRow : parentTimelineRow;
const wpId = this.extractWorkPackageId(hovered!);
- const tableRow:JQuery = this.tableAndTimeline.find(`tr.wp-row-${wpId}`).first();
- const timelineRow:JQuery = this.tableAndTimeline.find(`div.wp-row-${wpId}`).length
- ? this.tableAndTimeline.find(`div.wp-row-${wpId}`).first()
- : this.tableAndTimeline.find(`div.wp-ancestor-row-${wpId}`).first();
+ const tableRow = this.tableAndTimeline.querySelector(`tr.wp-row-${wpId}`);
+ const timelineRow = this.tableAndTimeline.querySelector(`div.wp-row-${wpId}`)
+ ? this.tableAndTimeline.querySelector(`div.wp-row-${wpId}`)
+ : this.tableAndTimeline.querySelector(`div.wp-ancestor-row-${wpId}`);
requestAnimationFrame(() => {
this.removeAllHoverClasses();
- timelineRow.addClass(cssClassRowHovered);
- tableRow.addClass(cssClassRowHovered);
+ timelineRow?.classList.add(cssClassRowHovered);
+ tableRow?.classList.add(cssClassRowHovered);
});
}
private removeAllHoverClasses() {
this.tableAndTimeline
- .find(`.${cssClassRowHovered}`)
- .removeClass(cssClassRowHovered);
+ .querySelectorAll(`.${cssClassRowHovered}`)
+ .forEach((elem) => elem.classList.remove(cssClassRowHovered));
}
}
diff --git a/frontend/src/app/features/work-packages/components/wp-table/wp-table-scroll-sync.ts b/frontend/src/app/features/work-packages/components/wp-table/wp-table-scroll-sync.ts
index 0a5540c3cbe6..672641714597 100644
--- a/frontend/src/app/features/work-packages/components/wp-table/wp-table-scroll-sync.ts
+++ b/frontend/src/app/features/work-packages/components/wp-table/wp-table-scroll-sync.ts
@@ -26,9 +26,11 @@
// See COPYRIGHT and LICENSE files for more details.
//++
+import { target } from 'core-app/shared/helpers/event-helpers';
+
export const selectorTableSide = '.work-packages-tabletimeline--table-side';
export const selectorTimelineSide = '.work-packages-tabletimeline--timeline-side';
-const jQueryScrollSyncEventNamespace = '.scroll-sync';
+const scrollSyncEventNamespace = '.scroll-sync';
const scrollStep = 15;
function getXandYScrollDeltas(ev:WheelEvent):[number, number] {
@@ -59,11 +61,9 @@ function getPlattformAgnosticScrollAmount(originalValue:number) {
return delta;
}
-function syncWheelEvent(jev:JQuery.TriggeredEvent, elementTable:JQuery, elementTimeline:JQuery) {
- const scrollTarget = jev.target;
- const ev:WheelEvent = jev.originalEvent as any;
+function syncWheelEvent(ev:WheelEvent, elementTable:HTMLElement, elementTimeline:HTMLElement) {
+ const scrollTarget = ev.target as HTMLElement;
let [deltaX, deltaY] = getXandYScrollDeltas(ev);
-
if (deltaY === 0) {
return;
}
@@ -72,8 +72,8 @@ function syncWheelEvent(jev:JQuery.TriggeredEvent, elementTable:JQuery, elementT
deltaY = getPlattformAgnosticScrollAmount(deltaY); // apply in both divs
window.requestAnimationFrame(() => {
- elementTable[0].scrollTop = elementTable[0].scrollTop + deltaY;
- elementTimeline[0].scrollTop = elementTable[0].scrollTop + deltaY;
+ elementTable.scrollTop = elementTable.scrollTop + deltaY;
+ elementTimeline.scrollTop = elementTable.scrollTop + deltaY;
scrollTarget.scrollLeft += deltaX;
});
@@ -82,11 +82,11 @@ function syncWheelEvent(jev:JQuery.TriggeredEvent, elementTable:JQuery, elementT
/**
* Activate or deactivate the scroll-sync between the table and timeline view.
*
- * @param $element true if the timeline is visible, false otherwise.
+ * @param element true if the timeline is visible, false otherwise.
*/
-export function createScrollSync($element:JQuery) {
- const elTable = jQuery($element).find(selectorTableSide);
- const elTimeline = jQuery($element).find(selectorTimelineSide);
+export function createScrollSync(element:HTMLElement) {
+ const elTable = element.querySelector(selectorTableSide)!;
+ const elTimeline = element.querySelector(selectorTimelineSide)!;
return (timelineVisible:boolean) => {
// state vars
@@ -95,13 +95,13 @@ export function createScrollSync($element:JQuery) {
if (timelineVisible) {
// setup event listener for table
- elTable.on(`wheel${jQueryScrollSyncEventNamespace}`, (jev:JQuery.TriggeredEvent) => {
- syncWheelEvent(jev, elTable, elTimeline);
+ target(elTable).on(`wheel${scrollSyncEventNamespace}`, (ev:WheelEvent) => {
+ syncWheelEvent(ev, elTable, elTimeline);
});
- elTable.on(`scroll${jQueryScrollSyncEventNamespace}`, (ev:JQuery.TriggeredEvent) => {
+ target(elTable).on(`scroll${scrollSyncEventNamespace}`, (ev:Event) => {
syncedLeft = true;
if (!syncedRight) {
- elTimeline[0].scrollTop = ev.target.scrollTop;
+ elTimeline.scrollTop = (ev.target as HTMLElement).scrollTop;
}
if (syncedLeft && syncedRight) {
syncedLeft = false;
@@ -110,13 +110,13 @@ export function createScrollSync($element:JQuery) {
});
// setup event listener for timeline
- elTimeline.on(`wheel${jQueryScrollSyncEventNamespace}`, (jev:JQuery.TriggeredEvent) => {
- syncWheelEvent(jev, elTable, elTimeline);
+ target(elTimeline).on(`wheel${scrollSyncEventNamespace}`, (ev:WheelEvent) => {
+ syncWheelEvent(ev, elTable, elTimeline);
});
- elTimeline.on(`scroll${jQueryScrollSyncEventNamespace}`, (ev:JQuery.TriggeredEvent) => {
+ target(elTimeline).on(`scroll${scrollSyncEventNamespace}`, (ev:Event) => {
syncedRight = true;
if (!syncedLeft) {
- elTable[0].scrollTop = ev.target.scrollTop;
+ elTable.scrollTop = (ev.target as HTMLElement).scrollTop;
}
if (syncedLeft && syncedRight) {
syncedLeft = false;
@@ -124,7 +124,7 @@ export function createScrollSync($element:JQuery) {
}
});
} else {
- elTable.off(jQueryScrollSyncEventNamespace);
+ target(elTable).off(scrollSyncEventNamespace);
}
};
}
diff --git a/frontend/src/app/features/work-packages/components/wp-table/wp-table.component.ts b/frontend/src/app/features/work-packages/components/wp-table/wp-table.component.ts
index dbe8d000662c..b8ca1a976596 100644
--- a/frontend/src/app/features/work-packages/components/wp-table/wp-table.component.ts
+++ b/frontend/src/app/features/work-packages/components/wp-table/wp-table.component.ts
@@ -95,7 +95,7 @@ export class WorkPackagesTableComponent extends UntilDestroyedMixin implements O
public configuration:WorkPackageTableConfiguration;
- private $element:JQuery;
+ private element:HTMLElement;
private scrollSyncUpdate:(timelineVisible:boolean) => any;
@@ -105,7 +105,7 @@ export class WorkPackagesTableComponent extends UntilDestroyedMixin implements O
public workPackageTable:WorkPackageTable;
- public tbody:JQuery;
+ public tbody:HTMLTableSectionElement;
public query:QueryResource;
@@ -157,7 +157,7 @@ export class WorkPackagesTableComponent extends UntilDestroyedMixin implements O
ngOnInit():void {
this.configuration = new WorkPackageTableConfiguration(this.configurationObject);
- this.$element = jQuery(this.elementRef.nativeElement);
+ this.element = this.elementRef.nativeElement;
// Clear any old table subscribers
this.querySpace.stopAllSubscriptions.next();
@@ -237,16 +237,16 @@ export class WorkPackagesTableComponent extends UntilDestroyedMixin implements O
}
public registerTimeline(controller:WorkPackageTimelineTableController, timelineBody:HTMLElement) {
- const tbody = this.$element.find('.work-package--results-tbody');
- const scrollContainer = this.$element.find('.work-package-table--container')[0];
+ const tbody = this.element.querySelector('.work-package--results-tbody')!;
+ const scrollContainer = this.element.querySelector('.work-package-table--container')!;
this.workPackageTable = new WorkPackageTable(
this.injector,
// Outer container for both table + Timeline
- this.$element[0],
+ this.element,
// Scroll container for the table/timeline
scrollContainer,
// Table tbody to insert into
- tbody[0],
+ tbody,
// Timeline body to insert into
timelineBody,
// Timeline controller
@@ -266,11 +266,11 @@ export class WorkPackagesTableComponent extends UntilDestroyedMixin implements O
this.timeline = tableAndTimeline[1];
// sync hover from table to timeline
- this.wpTableHoverSync = new WpTableHoverSync(this.$element);
+ this.wpTableHoverSync = new WpTableHoverSync(this.element);
this.wpTableHoverSync.activate();
// sync scroll from table to timeline
- this.scrollSyncUpdate = createScrollSync(this.$element);
+ this.scrollSyncUpdate = createScrollSync(this.element);
this.scrollSyncUpdate(this.timelineVisible);
this.cdRef.detectChanges();
@@ -281,13 +281,13 @@ export class WorkPackagesTableComponent extends UntilDestroyedMixin implements O
}
private getTableAndTimelineElement():[HTMLElement, HTMLElement] {
- const $tableSide = this.$element.find('.work-packages-tabletimeline--table-side');
- const $timelineSide = this.$element.find('.work-packages-tabletimeline--timeline-side');
+ const tableSide = this.element.querySelector('.work-packages-tabletimeline--table-side');
+ const timelineSide = this.element.querySelector('.work-packages-tabletimeline--timeline-side');
- if ($timelineSide.length === 0 || $tableSide.length === 0) {
+ if (!tableSide || !timelineSide) {
throw new Error('invalid state');
}
- return [$tableSide[0], $timelineSide[0]];
+ return [tableSide, timelineSide];
}
}
diff --git a/frontend/src/app/features/work-packages/routing/wp-view-base/event-handling/event-handler-registry.ts b/frontend/src/app/features/work-packages/routing/wp-view-base/event-handling/event-handler-registry.ts
index 292a8b348be7..636571d2d30b 100644
--- a/frontend/src/app/features/work-packages/routing/wp-view-base/event-handling/event-handler-registry.ts
+++ b/frontend/src/app/features/work-packages/routing/wp-view-base/event-handling/event-handler-registry.ts
@@ -1,17 +1,20 @@
import { EventEmitter, InjectionToken, Injector } from '@angular/core';
+import { delegate } from '@knowledgecode/delegate';
+
+export type EventType = keyof HTMLElementEventMap;
export interface WorkPackageViewEventHandler {
/** Event name to register * */
- EVENT:string;
+ EVENT:EventType|EventType[];
/** Event context CSS selector */
SELECTOR:string;
/** Event callback handler */
- handleEvent(view:T, evt:JQuery.TriggeredEvent):void;
+ handleEvent(view:T, evt:Event):void;
/** Event scope method */
- eventScope(view:T):JQuery;
+ eventScope(view:T):HTMLElement;
}
export interface WorkPackageViewOutputs {
@@ -40,9 +43,12 @@ export abstract class WorkPackageViewHandlerRegistry {
this.eventHandlers.map((factory) => {
const handler = factory(viewRef);
const target = handler.eventScope(viewRef);
+ const types = Array.isArray(handler.EVENT) ? handler.EVENT : [handler.EVENT];
- target.on(handler.EVENT, handler.SELECTOR, (evt:JQuery.TriggeredEvent) => {
- handler.handleEvent(viewRef, evt);
+ types.forEach((type) => {
+ delegate(target).on(type, handler.SELECTOR, (evt) => {
+ handler.handleEvent(viewRef, evt.originalEvent);
+ });
});
return handler;
diff --git a/frontend/src/app/shared/components/autocompleter/create-autocompleter/create-autocompleter.component.ts b/frontend/src/app/shared/components/autocompleter/create-autocompleter/create-autocompleter.component.ts
index 9f230cf1ce39..d1945641373c 100644
--- a/frontend/src/app/shared/components/autocompleter/create-autocompleter/create-autocompleter.component.ts
+++ b/frontend/src/app/shared/components/autocompleter/create-autocompleter/create-autocompleter.component.ts
@@ -88,7 +88,7 @@ export class CreateAutocompleterComponent extends UntilDestroyedMixin implements
@Output() public onChange = new EventEmitter();
- @Output() public onKeydown = new EventEmitter();
+ @Output() public onKeydown = new EventEmitter();
@Output() public onOpen = new EventEmitter();
@@ -158,7 +158,7 @@ export class CreateAutocompleterComponent extends UntilDestroyedMixin implements
this.onClose.emit();
}
- public keyPressed(event:JQuery.TriggeredEvent) {
+ public keyPressed(event:KeyboardEvent) {
this.onKeydown.emit(event);
}
diff --git a/frontend/src/app/shared/components/editable-toolbar-title/editable-toolbar-title.component.ts b/frontend/src/app/shared/components/editable-toolbar-title/editable-toolbar-title.component.ts
index 91f2183a1d73..6a10ca2d57c7 100644
--- a/frontend/src/app/shared/components/editable-toolbar-title/editable-toolbar-title.component.ts
+++ b/frontend/src/app/shared/components/editable-toolbar-title/editable-toolbar-title.component.ts
@@ -75,7 +75,7 @@ export class EditableToolbarTitleComponent implements OnInit, OnChanges {
return this.editable;
}
- @ViewChild('editableTitleInput') inputField?:ElementRef;
+ @ViewChild('editableTitleInput') inputField?:ElementRef;
public selectedTitle:string;
@@ -101,13 +101,13 @@ export class EditableToolbarTitleComponent implements OnInit, OnChanges {
ngOnInit():void {
this.text.input_title = `${this.text.click_to_edit} ${this.text.press_enter_to_save}`;
- jQuery(this.elementRef.nativeElement).on(triggerEditingEvent, (evt:Event, val = '') => {
+ this.elementRef.nativeElement.addEventListener(triggerEditingEvent, (evt:CustomEvent) => {
// In case we're not editable, ignore request
if (!this.inputField) {
return;
}
- this.selectedTitle = val;
+ this.selectedTitle = evt.detail ?? '';
setTimeout(() => {
const field:HTMLInputElement = this.inputField!.nativeElement;
field.focus();
@@ -123,7 +123,7 @@ export class EditableToolbarTitleComponent implements OnInit, OnChanges {
}
if (changes.initialFocus && changes.initialFocus.firstChange && this.inputField!) {
- const field:HTMLInputElement = this.inputField.nativeElement;
+ const field = this.inputField.nativeElement;
this.selectInputOnInitalFocus(field);
}
}
@@ -175,7 +175,7 @@ export class EditableToolbarTitleComponent implements OnInit, OnChanges {
// Blur this element
if (this.inputField) {
- (this.inputField.nativeElement as HTMLInputElement).blur();
+ this.inputField.nativeElement.blur();
}
// Avoid double saving
@@ -227,6 +227,6 @@ export class EditableToolbarTitleComponent implements OnInit, OnChanges {
}
private toggleToolbarButtonVisibility(hidden:boolean):void {
- jQuery('.toolbar-items').toggleClass('hidden-for-mobile', hidden);
+ document.querySelectorAll('.toolbar-items').forEach((toolbarItem) => toolbarItem.classList.toggle('hidden-for-mobile', hidden));
}
}
diff --git a/frontend/src/app/shared/components/editor/components/ckeditor/ckeditor-setup.service.ts b/frontend/src/app/shared/components/editor/components/ckeditor/ckeditor-setup.service.ts
index 87b7c69b066e..705674a3ed1f 100644
--- a/frontend/src/app/shared/components/editor/components/ckeditor/ckeditor-setup.service.ts
+++ b/frontend/src/app/shared/components/editor/components/ckeditor/ckeditor-setup.service.ts
@@ -73,12 +73,18 @@ export class CKEditorSetupService {
toolbarWrapper.appendChild(editor.ui.view.toolbar.element);
// Allow custom events on wrapper to set/get data for debugging
- jQuery(wrapper)
- // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access,@typescript-eslint/no-unsafe-call,@typescript-eslint/no-unsafe-return
- .on('op:ckeditor:autosave', () => editor.config.get('autosave').save(editor))
- .on('op:ckeditor:setData', (_, data:string) => editor.setData(data))
- .on('op:ckeditor:clear', () => editor.setData(' '))
- .on('op:ckeditor:getData', (_, cb:(data:string) => void) => cb(editor.getData({ trim: false })));
+ wrapper.addEventListener('op:ckeditor:autosave', () => {
+ editor.config.get('autosave').save(editor);
+ });
+ wrapper.addEventListener('op:ckeditor:setData', (event:CustomEvent) => {
+ editor.setData(event.detail);
+ });
+ wrapper.addEventListener('op:ckeditor:clear', () => {
+ editor.setData(' ');
+ });
+ wrapper.addEventListener('op:ckeditor:getData', (event:CustomEvent<(data:string) => void>) => {
+ event.detail(editor.getData({ trim: false }));
+ });
return watchdog;
});
diff --git a/frontend/src/app/shared/components/fields/display/display-field-renderer.ts b/frontend/src/app/shared/components/fields/display/display-field-renderer.ts
index d3b4c8cb716f..abb9ef8d6844 100644
--- a/frontend/src/app/shared/components/fields/display/display-field-renderer.ts
+++ b/frontend/src/app/shared/components/fields/display/display-field-renderer.ts
@@ -159,8 +159,10 @@ export class DisplayFieldRenderer {
if (field.isFormattable && !field.isEmpty()) {
try {
- titleContent = _.escape(jQuery(`${labelContent}
`).text());
- } catch (e) {
+ const parser = new DOMParser();
+ const doc = parser.parseFromString(labelContent, 'text/html');
+ titleContent = _.escape(doc.body.textContent ?? '');
+ } catch {
console.error('Failed to parse formattable labelContent');
titleContent = `Label for ${field.displayName}`;
}
diff --git a/frontend/src/app/shared/components/fields/edit/edit-field.component.ts b/frontend/src/app/shared/components/fields/edit/edit-field.component.ts
index 2293dc2cfd60..273afd450144 100644
--- a/frontend/src/app/shared/components/fields/edit/edit-field.component.ts
+++ b/frontend/src/app/shared/components/fields/edit/edit-field.component.ts
@@ -56,8 +56,7 @@ export abstract class EditFieldComponent extends Field implements OnInit, OnDest
/** Self reference */
public self = this;
- /** JQuery accessor to element ref */
- protected $element:JQuery;
+ protected element:HTMLElement;
constructor(
readonly I18n:I18nService,
@@ -74,7 +73,7 @@ export abstract class EditFieldComponent extends Field implements OnInit, OnDest
}
ngOnInit():void {
- this.$element = jQuery(this.elementRef.nativeElement as HTMLElement);
+ this.element = this.elementRef.nativeElement as HTMLElement;
this.initialize();
if (this.change.state) {
@@ -98,12 +97,9 @@ export abstract class EditFieldComponent extends Field implements OnInit, OnDest
}
public get overflowingSelector() {
- if (this.$element) {
- return this.$element
- .closest(overflowingContainerSelector)
- .data(overflowingContainerAttribute);
- }
- return null;
+ return this.element
+ ?.closest(overflowingContainerSelector)
+ ?.dataset[overflowingContainerAttribute];
}
public get inFlight() {
diff --git a/frontend/src/app/shared/components/fields/edit/edit-form/edit-form.component.ts b/frontend/src/app/shared/components/fields/edit/edit-form/edit-form.component.ts
index 321be3cada1b..6fc0d12939f9 100644
--- a/frontend/src/app/shared/components/fields/edit/edit-form/edit-form.component.ts
+++ b/frontend/src/app/shared/components/fields/edit/edit-form/edit-form.component.ts
@@ -204,10 +204,9 @@ export class EditFormComponent extends EditForm implements OnInit,
protected focusOnFirstError():void {
// Focus the first field that is erroneous
- jQuery(this.elementRef.nativeElement)
- .find(`.${activeFieldContainerClassName}.-error .${activeFieldClassName}`)
- .first()
- .trigger('focus');
+ this.elementRef.nativeElement
+ .querySelector(`.${activeFieldContainerClassName}.-error .${activeFieldClassName}`)
+ ?.focus();
}
private skipField(field:EditableAttributeFieldComponent) {
diff --git a/frontend/src/app/shared/components/fields/edit/editing-portal/edit-field-handler.ts b/frontend/src/app/shared/components/fields/edit/editing-portal/edit-field-handler.ts
index 2a78b1d51a4d..3a749e9088a7 100644
--- a/frontend/src/app/shared/components/fields/edit/editing-portal/edit-field-handler.ts
+++ b/frontend/src/app/shared/components/fields/edit/editing-portal/edit-field-handler.ts
@@ -103,7 +103,7 @@ export abstract class EditFieldHandler extends UntilDestroyedMixin {
/**
* Stop event propagation
*/
- public abstract stopPropagation(evt:JQuery.TriggeredEvent):boolean;
+ public abstract stopPropagation(evt:Event):boolean;
/**
* Focus on the active field.
@@ -122,7 +122,7 @@ export abstract class EditFieldHandler extends UntilDestroyedMixin {
* In an edit mode, we can't derive from a submit event whether the user pressed enter
* (and on what field he did that).
*/
- public abstract handleUserKeydown(event:JQuery.TriggeredEvent, onlyCancel?:boolean):void;
+ public abstract handleUserKeydown(event:KeyboardEvent, onlyCancel?:boolean):void;
/**
* Cancel edit
diff --git a/frontend/src/app/shared/components/fields/edit/field-handler/hal-resource-edit-field-handler.ts b/frontend/src/app/shared/components/fields/edit/field-handler/hal-resource-edit-field-handler.ts
index eeb68f3a6183..e041baa53b54 100644
--- a/frontend/src/app/shared/components/fields/edit/field-handler/hal-resource-edit-field-handler.ts
+++ b/frontend/src/app/shared/components/fields/edit/field-handler/hal-resource-edit-field-handler.ts
@@ -76,7 +76,7 @@ export class HalResourceEditFieldHandler extends EditFieldHandler {
/**
* Stop this event from propagating out of the edit field context.
*/
- public stopPropagation(evt:JQuery.TriggeredEvent) {
+ public stopPropagation(evt:Event) {
evt.stopPropagation();
return false;
}
@@ -135,7 +135,7 @@ export class HalResourceEditFieldHandler extends EditFieldHandler {
* In an edit mode, we can't derive from a submit event whether the user pressed enter
* (and on what field he did that).
*/
- public async handleUserKeydown(event:JQuery.TriggeredEvent, onlyCancel = false) {
+ public async handleUserKeydown(event:KeyboardEvent, onlyCancel = false) {
// Only handle submission in edit mode
if (this.inEditMode && !onlyCancel) {
if (event.key === 'Enter') {
diff --git a/frontend/src/app/shared/components/fields/edit/field-types/multi-select-edit-field.component.ts b/frontend/src/app/shared/components/fields/edit/field-types/multi-select-edit-field.component.ts
index f348d1c7da10..de6c024ca713 100644
--- a/frontend/src/app/shared/components/fields/edit/field-types/multi-select-edit-field.component.ts
+++ b/frontend/src/app/shared/components/fields/edit/field-types/multi-select-edit-field.component.ts
@@ -142,9 +142,9 @@ export class MultiSelectEditFieldComponent extends EditFieldComponent implements
}
public onOpen() {
- jQuery(this.hiddenOverflowContainer).one('scroll', () => {
+ document.querySelector(this.hiddenOverflowContainer)?.addEventListener('scroll', () => {
this.ngSelectComponent.close();
- });
+ }, { once: true });
}
public onClose() {
diff --git a/frontend/src/app/shared/components/fields/edit/field-types/project-status-edit-field.component.ts b/frontend/src/app/shared/components/fields/edit/field-types/project-status-edit-field.component.ts
index e45602807c7f..8cd45f227128 100644
--- a/frontend/src/app/shared/components/fields/edit/field-types/project-status-edit-field.component.ts
+++ b/frontend/src/app/shared/components/fields/edit/field-types/project-status-edit-field.component.ts
@@ -37,6 +37,7 @@ import {
import { InjectField } from 'core-app/shared/helpers/angular/inject-field.decorator';
import { HalResource } from 'core-app/features/hal/resources/hal-resource';
import { repositionDropdownBugfix } from 'core-app/shared/components/autocompleter/op-autocompleter/autocompleter.helper';
+import { target } from 'core-app/shared/helpers/event-helpers';
interface ProjectStatusOption {
href:string
@@ -96,12 +97,12 @@ export class ProjectStatusEditFieldComponent extends EditFieldComponent implemen
public onOpen() {
repositionDropdownBugfix(this.ngSelectComponent);
- jQuery(this.hiddenOverflowContainer).one('scroll.autocompleteContainer', () => {
+ target(document.querySelector(this.hiddenOverflowContainer)!).one('scroll.autocompleteContainer', () => {
this.ngSelectComponent.close();
});
}
public onClose() {
- jQuery(this.hiddenOverflowContainer).off('scroll.autocompleteContainer');
+ target(document.querySelector(this.hiddenOverflowContainer)!).off('scroll.autocompleteContainer');
}
}
diff --git a/frontend/src/app/shared/components/fields/edit/field-types/select-edit-field/select-edit-field.component.ts b/frontend/src/app/shared/components/fields/edit/field-types/select-edit-field/select-edit-field.component.ts
index c8ec37c91ee5..443b0ece73b4 100644
--- a/frontend/src/app/shared/components/fields/edit/field-types/select-edit-field/select-edit-field.component.ts
+++ b/frontend/src/app/shared/components/fields/edit/field-types/select-edit-field/select-edit-field.component.ts
@@ -79,7 +79,7 @@ export class SelectEditFieldComponent extends EditFieldComponent implements OnIn
onCreate: (newElement:HalResource) => this.onCreate(newElement),
onChange: (value:HalResource) => this.onChange(value),
onAddNew: (value:HalResource) => this.onNewValueAdded(value),
- onKeydown: (event:JQuery.TriggeredEvent) => this.handler.handleUserKeydown(event, true),
+ onKeydown: (event:KeyboardEvent) => this.handler.handleUserKeydown(event, true),
onOpen: () => this.onOpen(),
onClose: () => this.onClose(),
onAfterViewInit: (component:CreateAutocompleterComponent) => this._autocompleterComponent = component,
@@ -230,9 +230,9 @@ export class SelectEditFieldComponent extends EditFieldComponent implements OnIn
}
public onOpen() {
- jQuery(this.hiddenOverflowContainer).one('scroll', () => {
+ document.querySelector(this.hiddenOverflowContainer)!.addEventListener('scroll', () => {
this._autocompleterComponent.closeSelect();
- });
+ }, { once: true });
}
public onClose() {
diff --git a/frontend/src/app/shared/components/fields/edit/field/editable-attribute-field.component.ts b/frontend/src/app/shared/components/fields/edit/field/editable-attribute-field.component.ts
index c69afa4ea97f..608276ab79e5 100644
--- a/frontend/src/app/shared/components/fields/edit/field/editable-attribute-field.component.ts
+++ b/frontend/src/app/shared/components/fields/edit/field/editable-attribute-field.component.ts
@@ -86,7 +86,7 @@ export class EditableAttributeFieldComponent extends UntilDestroyedMixin impleme
public active = false;
- private $element:JQuery;
+ private element:HTMLElement;
public destroyed = false;
@@ -114,7 +114,7 @@ export class EditableAttributeFieldComponent extends UntilDestroyedMixin impleme
public ngOnInit():void {
this.fieldRenderer = new DisplayFieldRenderer(this.injector, 'single-view', this.displayFieldOptions);
- this.$element = jQuery(this.elementRef.nativeElement);
+ this.element = this.elementRef.nativeElement;
// Register on the form if we're in an editable context
this.editForm?.register(this);
@@ -157,7 +157,7 @@ export class EditableAttributeFieldComponent extends UntilDestroyedMixin impleme
this.setActive(false);
if (focus) {
- setTimeout(() => this.$element.find(`.${displayClassName}`).focus(), 20);
+ setTimeout(() => this.element.querySelector(`.${displayClassName}`)?.focus(), 20);
}
}
@@ -174,8 +174,9 @@ export class EditableAttributeFieldComponent extends UntilDestroyedMixin impleme
}
// Skip activation if the user clicked on a link or within a macro
- const target = jQuery(event.target as HTMLElement);
- if (target.closest(`a:not(.${displayTriggerLink}),macro`, this.displayContainer.nativeElement).length > 0) {
+ const target = event.target as HTMLElement;
+ const foundElement = target.closest(`a:not(.${displayTriggerLink}),macro`);
+ if (foundElement && this.displayContainer.nativeElement.contains(foundElement)) {
return true;
}
@@ -211,7 +212,7 @@ export class EditableAttributeFieldComponent extends UntilDestroyedMixin impleme
// This can be both a direct click as well as a "click" via keyboard, e.g. the key.
if (evt?.type === 'click') {
// Get the position where the user clicked.
- positionOffset = getPosition(evt);
+ positionOffset = getPosition(evt as MouseEvent);
}
void this.activateOnForm()
@@ -241,3 +242,4 @@ export class EditableAttributeFieldComponent extends UntilDestroyedMixin impleme
return this.schemaCache.of(this.resource);
}
}
+
diff --git a/frontend/src/app/shared/components/grids/grid/area.service.ts b/frontend/src/app/shared/components/grids/grid/area.service.ts
index b29d79a95ee5..4875ebeb5d23 100644
--- a/frontend/src/app/shared/components/grids/grid/area.service.ts
+++ b/frontend/src/app/shared/components/grids/grid/area.service.ts
@@ -150,10 +150,10 @@ export class GridAreaService {
// But as scrollIntoView will always readjust the viewport, the result would be an unbearable flicker
// which causes e.g. dragging to be impossible.
public scrollPlaceholderIntoView() {
- const placeholder = jQuery('.grid--area.-placeholder');
+ const placeholder = document.querySelector('.grid--area.-placeholder');
- if ((placeholder[0] as any).scrollIntoViewIfNeeded) {
- setTimeout(() => (placeholder[0] as any).scrollIntoViewIfNeeded());
+ if ((placeholder as any).scrollIntoViewIfNeeded) {
+ setTimeout(() => (placeholder as any).scrollIntoViewIfNeeded());
}
}
diff --git a/frontend/src/app/shared/components/grids/widgets/add/add.modal.ts b/frontend/src/app/shared/components/grids/widgets/add/add.modal.ts
index f535f0c3eb66..dd6a474db128 100644
--- a/frontend/src/app/shared/components/grids/widgets/add/add.modal.ts
+++ b/frontend/src/app/shared/components/grids/widgets/add/add.modal.ts
@@ -54,9 +54,9 @@ export class AddGridWidgetModalComponent extends OpModalComponent implements OnI
return this.eligibleWidgets.sort((a, b) => a.title.localeCompare(b.title));
}
- public select($event:MouseEvent, widget:WidgetRegistration) {
+ public select(event:MouseEvent, widget:WidgetRegistration) {
this.chosenWidget = widget;
- this.closeMe($event);
+ this.closeMe(event);
}
public trackWidgetBy(_index:number, widget:WidgetRegistration) {
diff --git a/frontend/src/app/shared/components/grids/widgets/custom-text/custom-text.component.ts b/frontend/src/app/shared/components/grids/widgets/custom-text/custom-text.component.ts
index 430294297cd1..dda9fb185440 100644
--- a/frontend/src/app/shared/components/grids/widgets/custom-text/custom-text.component.ts
+++ b/frontend/src/app/shared/components/grids/widgets/custom-text/custom-text.component.ts
@@ -38,7 +38,7 @@ export class WidgetCustomTextComponent extends AbstractWidgetComponent implement
attachments: this.I18n.t('js.label_attachments'),
};
- @ViewChild('displayContainer') readonly displayContainer:ElementRef;
+ @ViewChild('displayContainer') readonly displayContainer:ElementRef;
constructor(
protected I18n:I18nService,
diff --git a/frontend/src/app/shared/components/header-project-select/list/header-project-select-list.component.ts b/frontend/src/app/shared/components/header-project-select/list/header-project-select-list.component.ts
index 1117f73362df..e8af8ce0abe5 100644
--- a/frontend/src/app/shared/components/header-project-select/list/header-project-select-list.component.ts
+++ b/frontend/src/app/shared/components/header-project-select/list/header-project-select-list.component.ts
@@ -19,6 +19,7 @@ import { IProjectData } from 'core-app/shared/components/searchable-project-list
import { PathHelperService } from 'core-app/core/path-helper/path-helper.service';
import { ConfigurationService } from 'core-app/core/config/configuration.service';
import { CurrentProjectService } from 'core-app/core/current-project/current-project.service';
+import { getMetaContent } from 'core-app/core/setup/globals/global-helpers';
@Component({
selector: '[op-header-project-select-list]',
@@ -107,13 +108,13 @@ export class OpHeaderProjectSelectListComponent implements OnInit, OnChanges {
}
extendedUrl(projectId:string|null):string {
- const currentMenuItem = document.querySelector('meta[name="current_menu_item"]')!;
+ const currentMenuItem = getMetaContent('current_menu_item');
const url = projectId === null ? window.appBasePath : this.pathHelper.projectPath(projectId);
if (!currentMenuItem) {
return url;
}
- return `${url}?jump=${encodeURIComponent(currentMenuItem.content)}`;
+ return `${url}?jump=${encodeURIComponent(currentMenuItem)}`;
}
}
diff --git a/frontend/src/app/shared/components/modal/modal-wrapper-augment.service.ts b/frontend/src/app/shared/components/modal/modal-wrapper-augment.service.ts
index dd3d99145970..0745a70867ba 100644
--- a/frontend/src/app/shared/components/modal/modal-wrapper-augment.service.ts
+++ b/frontend/src/app/shared/components/modal/modal-wrapper-augment.service.ts
@@ -50,37 +50,36 @@ export class OpModalWrapperAugmentService {
public setupListener() {
const matches = this.documentElement.querySelectorAll('[data-augmented-model-wrapper]');
for (let i = 0; i < matches.length; ++i) {
- this.wrapElement(jQuery(matches[i]) as JQuery);
+ this.wrapElement(matches[i] as HTMLElement);
}
}
/**
* Wrap a section[data-augmented-modal-wrapper] element
*/
- public wrapElement(element:JQuery) {
+ public wrapElement(element:HTMLElement) {
// Find activation link
- const activationSelector = element.data('activationSelector') || '.modal-delivery-element--activation-link';
- const activationLink = jQuery(activationSelector);
-
- const initializeNow = element.data('modalInitializeNow');
+ const activationSelector = element.dataset.activationSelector || '.modal-delivery-element--activation-link';
+ const activationLink = document.querySelector(activationSelector);
+ const initializeNow = element.dataset.modalInitializeNow;
if (initializeNow) {
this.show(element);
} else {
- activationLink.click((evt:JQuery.TriggeredEvent) => {
+ activationLink?.addEventListener('click', (evt) => {
this.show(element);
evt.preventDefault();
});
}
}
- private show(element:JQuery) {
+ private show(element:HTMLElement) {
// Set modal class name
- const modalClassName = element.data('modalClassName');
+ const modalClassName = element.dataset.modalClassName;
// Set template from wrapped element
- const wrappedElement = element.find('.modal-delivery-element');
- let modalBody = wrappedElement.html();
+ const wrappedElement = element.querySelector('.modal-delivery-element')!;
+ let modalBody = wrappedElement.innerHTML;
this.opModalService.show(
DynamicContentModalComponent,
diff --git a/frontend/src/app/shared/components/modal/modal.component.ts b/frontend/src/app/shared/components/modal/modal.component.ts
index 62d5ab175fdf..09ea32300b07 100644
--- a/frontend/src/app/shared/components/modal/modal.component.ts
+++ b/frontend/src/app/shared/components/modal/modal.component.ts
@@ -38,7 +38,7 @@ export abstract class OpModalComponent extends UntilDestroyedMixin implements On
/* Reference to service */
protected service:OpModalService = this.locals.service;
- public $element:HTMLElement;
+ public element:HTMLElement;
/** Closing event called from the service when closing this modal */
public closingEvent = new EventEmitter();
@@ -60,7 +60,7 @@ export abstract class OpModalComponent extends UntilDestroyedMixin implements On
}
ngOnInit():void {
- this.$element = this.elementRef.nativeElement as HTMLElement;
+ this.element = this.elementRef.nativeElement as HTMLElement;
}
ngOnDestroy():void {
@@ -100,7 +100,7 @@ export abstract class OpModalComponent extends UntilDestroyedMixin implements On
}
protected get afterFocusOn():HTMLElement {
- return this.$element;
+ return this.element;
}
private onResize = debounce(() => this.updateAppHeight(), 10);
diff --git a/frontend/src/app/shared/components/modals/modal-wrapper/dynamic-content.modal.ts b/frontend/src/app/shared/components/modals/modal-wrapper/dynamic-content.modal.ts
index d6646321ec11..1e65ee532de8 100644
--- a/frontend/src/app/shared/components/modals/modal-wrapper/dynamic-content.modal.ts
+++ b/frontend/src/app/shared/components/modals/modal-wrapper/dynamic-content.modal.ts
@@ -59,7 +59,7 @@ export class DynamicContentModalComponent extends OpModalComponent implements On
super.ngOnInit();
// Append the dynamic body
- const wrapper = this.$element.children[0];
+ const wrapper = this.element.children[0];
const classes = (this.locals.modalClassName as string) || '';
wrapper.classList.add(...classes.split(' '));
wrapper.innerHTML = this.locals.modalBody as string;
diff --git a/frontend/src/app/shared/components/op-context-menu/handlers/op-columns-context-menu.directive.ts b/frontend/src/app/shared/components/op-context-menu/handlers/op-columns-context-menu.directive.ts
index 2201f15d7cc0..36746d38b6de 100644
--- a/frontend/src/app/shared/components/op-context-menu/handlers/op-columns-context-menu.directive.ts
+++ b/frontend/src/app/shared/components/op-context-menu/handlers/op-columns-context-menu.directive.ts
@@ -43,6 +43,7 @@ import { QueryColumn } from 'core-app/features/work-packages/components/wp-query
import { WpTableConfigurationModalComponent } from 'core-app/features/work-packages/components/wp-table/configuration-modal/wp-table-configuration.modal';
import { QUERY_SORT_BY_ASC, QUERY_SORT_BY_DESC } from 'core-app/features/hal/resources/query-sort-by-resource';
import { ConfirmDialogService } from 'core-app/shared/components/modals/confirm-dialog/confirm-dialog.service';
+import { computePosition, ComputePositionReturn, flip, shift } from '@floating-ui/dom';
@Directive({
selector: '[opColumnsContextMenu]',
@@ -73,7 +74,7 @@ export class OpColumnsContextMenu extends OpContextMenuTrigger {
super(elementRef, opContextMenu);
}
- protected open(evt:JQuery.TriggeredEvent) {
+ protected open(evt:Event) {
if (!this.table.configuration.columnMenuEnabled) {
return;
}
@@ -89,24 +90,19 @@ export class OpColumnsContextMenu extends OpContextMenuTrigger {
};
}
- /**
- * Positioning args for jquery-ui position.
- *
- * @param {Event} openerEvent
- */
- public positionArgs(evt:JQuery.TriggeredEvent) {
- const additionalPositionArgs = {
- of: this.$element.find('.generic-table--sort-header-outer'),
- };
-
- const position = super.positionArgs(evt);
- _.assign(position, additionalPositionArgs);
-
- return position;
+ public computePosition(floating:HTMLElement, openerEvent:Event):Promise {
+ const reference = this.element.querySelector('.generic-table--sort-header-outer')!;
+ return computePosition(reference, floating, {
+ placement: this.placement,
+ middleware: [
+ flip(),
+ shift({ padding: 10 }),
+ ],
+ });
}
- protected get afterFocusOn():JQuery {
- return this.$element.find(`#${this.column.id}`);
+ protected get afterFocusOn() {
+ return this.element.querySelector(`#${this.column.id}`)!;
}
private buildItems() {
@@ -194,7 +190,7 @@ export class OpColumnsContextMenu extends OpContextMenuTrigger {
setTimeout(() => {
if (focusColumn) {
- jQuery(`#${focusColumn.id}`).focus();
+ document.querySelector(`#${focusColumn.id}`)?.focus();
}
});
return true;
diff --git a/frontend/src/app/shared/components/op-context-menu/handlers/op-context-menu-trigger.directive.ts b/frontend/src/app/shared/components/op-context-menu/handlers/op-context-menu-trigger.directive.ts
index 8baa43b53fd5..37a1abf8d20c 100644
--- a/frontend/src/app/shared/components/op-context-menu/handlers/op-context-menu-trigger.directive.ts
+++ b/frontend/src/app/shared/components/op-context-menu/handlers/op-context-menu-trigger.directive.ts
@@ -3,13 +3,14 @@ import { OPContextMenuService } from 'core-app/shared/components/op-context-menu
import { OpContextMenuHandler } from 'core-app/shared/components/op-context-menu/op-context-menu-handler';
import { OpContextMenuItem } from 'core-app/shared/components/op-context-menu/op-context-menu.types';
import Mousetrap from 'mousetrap';
+import { computePosition, ComputePositionReturn, flip, shift } from '@floating-ui/dom';
@Directive({
selector: '[opContextMenuTrigger]',
standalone: false,
})
export class OpContextMenuTrigger extends OpContextMenuHandler implements AfterViewInit {
- protected $element:JQuery;
+ protected element:HTMLElement;
protected items:OpContextMenuItem[] = [];
@@ -21,10 +22,10 @@ export class OpContextMenuTrigger extends OpContextMenuHandler implements AfterV
}
ngAfterViewInit():void {
- this.$element = jQuery(this.elementRef.nativeElement);
+ this.element = this.elementRef.nativeElement;
// Open by clicking the element
- this.$element.on('click', (evt:JQuery.TriggeredEvent) => {
+ this.element.addEventListener('click', (evt) => {
evt.preventDefault();
// When clicking the same trigger twice, close the element instead.
@@ -36,22 +37,18 @@ export class OpContextMenuTrigger extends OpContextMenuHandler implements AfterV
});
// Open with keyboard combination as well
- Mousetrap(this.$element[0]).bind('shift+alt+f10', (evt:any) => {
+ Mousetrap(this.element).bind('shift+alt+f10', (evt:any) => {
this.open(evt);
});
}
- /**
- * Positioning args for jquery-ui position.
- *
- * @param {Event} openerEvent
- */
- public positionArgs(openerEvent:JQuery.TriggeredEvent) {
- return {
- my: 'left top',
- at: 'left bottom',
- of: this.$element,
- collision: 'flipfit',
- };
+ public computePosition(floating:HTMLElement, openerEvent:Event):Promise {
+ return computePosition(this.element, floating, {
+ placement: this.placement,
+ middleware: [
+ flip(),
+ shift({ padding: 10 }),
+ ],
+ });
}
}
diff --git a/frontend/src/app/shared/components/op-context-menu/handlers/op-settings-dropdown-menu.directive.ts b/frontend/src/app/shared/components/op-context-menu/handlers/op-settings-dropdown-menu.directive.ts
index 7ecd36cfad17..fe1939ea3a5b 100644
--- a/frontend/src/app/shared/components/op-context-menu/handlers/op-settings-dropdown-menu.directive.ts
+++ b/frontend/src/app/shared/components/op-context-menu/handlers/op-settings-dropdown-menu.directive.ts
@@ -77,6 +77,8 @@ export class OpSettingsMenuDirective extends OpContextMenuTrigger {
private loadingPromise:PromiseLike;
+ override readonly placement = 'bottom-end';
+
constructor(
readonly elementRef:ElementRef,
readonly opContextMenu:OPContextMenuService,
@@ -117,7 +119,7 @@ export class OpSettingsMenuDirective extends OpContextMenuTrigger {
});
}
- protected open(evt:JQuery.TriggeredEvent) {
+ protected open(evt:Event) {
this.loadingPromise.then(() => {
this.buildItems();
this.opContextMenu.show(this, evt);
@@ -131,38 +133,21 @@ export class OpSettingsMenuDirective extends OpContextMenuTrigger {
};
}
- /**
- * Positioning args for jquery-ui position.
- *
- * @param {Event} openerEvent
- */
- public positionArgs(evt:JQuery.TriggeredEvent) {
- const additionalPositionArgs = {
- my: 'right top',
- at: 'right bottom',
- };
-
- const position = super.positionArgs(evt);
- _.assign(position, additionalPositionArgs);
-
- return position;
- }
-
public onClose(focus:boolean) {
if (focus) {
this.afterFocusOn.focus();
}
}
- private allowQueryAction(event:JQuery.TriggeredEvent, action:any) {
+ private allowQueryAction(event:Event, action:any) {
return this.allowAction(event, 'query', action);
}
- private allowWorkPackageAction(event:JQuery.TriggeredEvent, action:any) {
+ private allowWorkPackageAction(event:Event, action:any) {
return this.allowAction(event, 'work_packages', action);
}
- private allowFormAction(event:JQuery.TriggeredEvent, action:string) {
+ private allowFormAction(event:Event, action:string) {
if (this.form.$links[action]) {
return true;
}
@@ -170,7 +155,7 @@ export class OpSettingsMenuDirective extends OpContextMenuTrigger {
return false;
}
- private allowAction(event:JQuery.TriggeredEvent, modelName:string, action:any) {
+ private allowAction(event:Event, modelName:string, action:any) {
if (this.authorisationService.can(modelName, action)) {
return true;
}
@@ -213,7 +198,7 @@ export class OpSettingsMenuDirective extends OpContextMenuTrigger {
linkText: this.I18n.t('js.toolbar.settings.configure_view'),
hidden: this.hideTableOptions,
icon: 'icon-settings',
- onClick: ($event:JQuery.TriggeredEvent) => {
+ onClick: (event:MouseEvent) => {
this.opContextMenu.close();
this.opModalService.show(WpTableConfigurationModalComponent, this.injector);
@@ -269,9 +254,9 @@ export class OpSettingsMenuDirective extends OpContextMenuTrigger {
disabled: !this.query.id || this.authorisationService.cannot('query', 'updateImmediately'),
linkText: this.I18n.t('js.toolbar.settings.page_settings'),
icon: 'icon-edit',
- onClick: ($event:JQuery.TriggeredEvent) => {
- if (this.allowQueryAction($event, 'update')) {
- jQuery(`${selectableTitleIdentifier}`).trigger(triggerEditingEvent);
+ onClick: (event:MouseEvent) => {
+ if (this.allowQueryAction(event, 'update')) {
+ document.querySelector(`${selectableTitleIdentifier}`)?.dispatchEvent(new CustomEvent(triggerEditingEvent, { bubbles: true }));
}
return true;
@@ -282,11 +267,11 @@ export class OpSettingsMenuDirective extends OpContextMenuTrigger {
disabled: this.authorisationService.cannot('query', 'updateImmediately'),
linkText: this.I18n.t('js.toolbar.settings.save'),
icon: 'icon-save',
- onClick: ($event:JQuery.TriggeredEvent) => {
+ onClick: (event) => {
const { query } = this;
- if (!isPersistedResource(query) && this.allowQueryAction($event, 'updateImmediately')) {
+ if (!isPersistedResource(query) && this.allowQueryAction(event, 'updateImmediately')) {
this.opModalService.show(SaveQueryModalComponent, this.injector);
- } else if (query.id && this.allowQueryAction($event, 'updateImmediately')) {
+ } else if (query.id && this.allowQueryAction(event, 'updateImmediately')) {
this.wpListService.save(query);
}
@@ -298,8 +283,8 @@ export class OpSettingsMenuDirective extends OpContextMenuTrigger {
disabled: this.form ? !this.form.$links.create_new : this.authorisationService.cannot('query', 'updateImmediately'),
linkText: this.I18n.t('js.toolbar.settings.save_as'),
icon: 'icon-save',
- onClick: ($event:JQuery.TriggeredEvent) => {
- if (this.allowFormAction($event, 'create_new')) {
+ onClick: (event) => {
+ if (this.allowFormAction(event, 'create_new')) {
this.opModalService.show(SaveQueryModalComponent, this.injector);
}
@@ -311,8 +296,8 @@ export class OpSettingsMenuDirective extends OpContextMenuTrigger {
disabled: this.authorisationService.cannot('query', 'delete'),
linkText: this.I18n.t('js.toolbar.settings.delete'),
icon: 'icon-delete',
- onClick: ($event:JQuery.TriggeredEvent) => {
- if (this.allowQueryAction($event, 'delete')
+ onClick: (event) => {
+ if (this.allowQueryAction(event, 'delete')
&& window.confirm(this.I18n.t('js.text_query_destroy_confirmation'))) {
this.wpListService.delete();
}
@@ -340,8 +325,8 @@ export class OpSettingsMenuDirective extends OpContextMenuTrigger {
linkText: this.I18n.t('js.toolbar.settings.export'),
hidden: this.hideTableOptions,
icon: 'icon-export',
- onClick: ($event:JQuery.TriggeredEvent) => {
- if (this.allowWorkPackageAction($event, 'representations')) {
+ onClick: (event) => {
+ if (this.allowWorkPackageAction(event, 'representations')) {
const query = this.querySpace.query.value;
if (query) {
const href = this.buildExportDialogHref(query);
@@ -356,8 +341,8 @@ export class OpSettingsMenuDirective extends OpContextMenuTrigger {
disabled: this.authorisationService.cannot('query', 'unstar') && this.authorisationService.cannot('query', 'star'),
linkText: this.I18n.t('js.toolbar.settings.visibility_settings'),
icon: 'icon-watched',
- onClick: ($event:JQuery.TriggeredEvent) => {
- if (this.allowQueryAction($event, 'unstar') || this.allowQueryAction($event, 'star')) {
+ onClick: (event) => {
+ if (this.allowQueryAction(event, 'unstar') || this.allowQueryAction(event, 'star')) {
this.opModalService.show(QuerySharingModalComponent, this.injector);
}
diff --git a/frontend/src/app/shared/components/op-context-menu/handlers/op-types-context-menu.directive.ts b/frontend/src/app/shared/components/op-context-menu/handlers/op-types-context-menu.directive.ts
index 41816c51bad6..fd1f33e54c74 100644
--- a/frontend/src/app/shared/components/op-context-menu/handlers/op-types-context-menu.directive.ts
+++ b/frontend/src/app/shared/components/op-context-menu/handlers/op-types-context-menu.directive.ts
@@ -77,7 +77,7 @@ export class OpTypesContextMenuDirective extends OpContextMenuTrigger {
}
}
- protected open(evt:JQuery.TriggeredEvent) {
+ protected open(evt:Event) {
this.isOpen = !this.isOpen;
if (this.isOpen) {
void this
@@ -112,10 +112,10 @@ export class OpTypesContextMenuDirective extends OpContextMenuTrigger {
href: this.$state.href(this.stateName, { type: type.id! }),
ariaLabel: type.name,
class: Highlighting.inlineClass('type', type.id!),
- onClick: ($event:JQuery.TriggeredEvent) => {
+ onClick: (event:MouseEvent) => {
if (this.routedFromAngular) {
this.isOpen = false;
- if (isClickedWithModifier($event)) {
+ if (isClickedWithModifier(event)) {
return false;
}
diff --git a/frontend/src/app/shared/components/op-context-menu/handlers/wp-create-settings-menu.directive.ts b/frontend/src/app/shared/components/op-context-menu/handlers/wp-create-settings-menu.directive.ts
index 72da3016a743..cded83e7fa9e 100644
--- a/frontend/src/app/shared/components/op-context-menu/handlers/wp-create-settings-menu.directive.ts
+++ b/frontend/src/app/shared/components/op-context-menu/handlers/wp-create-settings-menu.directive.ts
@@ -38,6 +38,8 @@ import { FormResource } from 'core-app/features/hal/resources/form-resource';
standalone: false,
})
export class WorkPackageCreateSettingsMenuDirective extends OpContextMenuTrigger {
+ override readonly placement = 'bottom-end';
+
constructor(readonly elementRef:ElementRef,
readonly opContextMenu:OPContextMenuService,
readonly states:States,
@@ -45,7 +47,7 @@ export class WorkPackageCreateSettingsMenuDirective extends OpContextMenuTrigger
super(elementRef, opContextMenu);
}
- protected open(evt:JQuery.TriggeredEvent) {
+ protected open(evt:Event) {
const wp = this.states.workPackages.get('new').value;
if (wp) {
@@ -59,23 +61,6 @@ export class WorkPackageCreateSettingsMenuDirective extends OpContextMenuTrigger
}
}
- /**
- * Positioning args for jquery-ui position.
- *
- * @param {Event} openerEvent
- */
- public positionArgs(evt:JQuery.TriggeredEvent) {
- const additionalPositionArgs = {
- my: 'right top',
- at: 'right bottom',
- };
-
- const position = super.positionArgs(evt);
- _.assign(position, additionalPositionArgs);
-
- return position;
- }
-
private buildItems(form:FormResource) {
this.items = [];
const configureFormLink = form.configureForm;
diff --git a/frontend/src/app/shared/components/op-context-menu/handlers/wp-group-toggle-dropdown-menu.directive.ts b/frontend/src/app/shared/components/op-context-menu/handlers/wp-group-toggle-dropdown-menu.directive.ts
index 879feabe5811..56087eca2ebe 100644
--- a/frontend/src/app/shared/components/op-context-menu/handlers/wp-group-toggle-dropdown-menu.directive.ts
+++ b/frontend/src/app/shared/components/op-context-menu/handlers/wp-group-toggle-dropdown-menu.directive.ts
@@ -44,7 +44,7 @@ export class WorkPackageGroupToggleDropdownMenuDirective extends OpContextMenuTr
super(elementRef, opContextMenu);
}
- protected open(evt:JQuery.TriggeredEvent) {
+ protected open(evt:Event) {
this.buildItems();
this.opContextMenu.show(this, evt);
}
@@ -62,7 +62,7 @@ export class WorkPackageGroupToggleDropdownMenuDirective extends OpContextMenuTr
disabled: this.wpViewCollapsedGroups.allGroupsAreCollapsed,
linkText: this.I18n.t('js.button_collapse_all'),
icon: 'icon-minus2',
- onClick: (evt:JQuery.TriggeredEvent) => {
+ onClick: (evt) => {
this.wpViewCollapsedGroups.setAllGroupsCollapseStateTo(true);
return true;
@@ -72,7 +72,7 @@ export class WorkPackageGroupToggleDropdownMenuDirective extends OpContextMenuTr
disabled: this.wpViewCollapsedGroups.allGroupsAreExpanded,
linkText: this.I18n.t('js.button_expand_all'),
icon: 'icon-plus',
- onClick: (evt:JQuery.TriggeredEvent) => {
+ onClick: (evt) => {
this.wpViewCollapsedGroups.setAllGroupsCollapseStateTo(false);
return true;
diff --git a/frontend/src/app/shared/components/op-context-menu/handlers/wp-status-dropdown-menu.directive.ts b/frontend/src/app/shared/components/op-context-menu/handlers/wp-status-dropdown-menu.directive.ts
index 93f8851d1e36..630b94b7e7c6 100644
--- a/frontend/src/app/shared/components/op-context-menu/handlers/wp-status-dropdown-menu.directive.ts
+++ b/frontend/src/app/shared/components/op-context-menu/handlers/wp-status-dropdown-menu.directive.ts
@@ -69,7 +69,7 @@ export class WorkPackageStatusDropdownDirective extends OpContextMenuTrigger {
super(elementRef, opContextMenu);
}
- protected open(evt:JQuery.TriggeredEvent) {
+ protected open(evt:Event) {
const change = this.halEditing.changeFor(this.workPackage);
change.getForm().then((form:any) => {
diff --git a/frontend/src/app/shared/components/op-context-menu/handlers/wp-view-dropdown-menu.directive.ts b/frontend/src/app/shared/components/op-context-menu/handlers/wp-view-dropdown-menu.directive.ts
index bd7941568af9..83ab62e6d0a0 100644
--- a/frontend/src/app/shared/components/op-context-menu/handlers/wp-view-dropdown-menu.directive.ts
+++ b/frontend/src/app/shared/components/op-context-menu/handlers/wp-view-dropdown-menu.directive.ts
@@ -54,7 +54,7 @@ export class WorkPackageViewDropdownMenuDirective extends OpContextMenuTrigger {
public isOpen = false;
- protected open(evt:JQuery.TriggeredEvent) {
+ protected open(evt:Event) {
this.isOpen = !this.isOpen;
if (this.isOpen) {
this.buildItems();
diff --git a/frontend/src/app/shared/components/op-context-menu/icon-triggered-context-menu/icon-triggered-context-menu.component.ts b/frontend/src/app/shared/components/op-context-menu/icon-triggered-context-menu/icon-triggered-context-menu.component.ts
index e3ee84b48842..dd5a4cf30a45 100644
--- a/frontend/src/app/shared/components/op-context-menu/icon-triggered-context-menu/icon-triggered-context-menu.component.ts
+++ b/frontend/src/app/shared/components/op-context-menu/icon-triggered-context-menu/icon-triggered-context-menu.component.ts
@@ -42,6 +42,8 @@ import { OpContextMenuItem } from 'core-app/shared/components/op-context-menu/op
standalone: false,
})
export class IconTriggeredContextMenuComponent extends OpContextMenuTrigger {
+ override readonly placement = 'bottom-end';
+
constructor(
readonly elementRef:ElementRef,
readonly opContextMenu:OPContextMenuService,
@@ -55,28 +57,11 @@ export class IconTriggeredContextMenuComponent extends OpContextMenuTrigger {
@Input() menuItemsFactory:() => Promise;
- protected async open(evt:JQuery.TriggeredEvent) {
+ protected async open(evt:Event) {
this.items = await this.buildItems();
this.opContextMenu.show(this, evt);
}
- /**
- * Positioning args for jquery-ui position.
- *
- * @param {Event} openerEvent
- */
- public positionArgs(evt:JQuery.TriggeredEvent) {
- const additionalPositionArgs = {
- my: 'right top',
- at: 'right bottom',
- };
-
- const position = super.positionArgs(evt);
- _.assign(position, additionalPositionArgs);
-
- return position;
- }
-
private async buildItems() {
const items:OpContextMenuItem[] = [];
diff --git a/frontend/src/app/shared/components/op-context-menu/op-context-menu-handler.ts b/frontend/src/app/shared/components/op-context-menu/op-context-menu-handler.ts
index 4e5dcc151399..d02ac296ccbb 100644
--- a/frontend/src/app/shared/components/op-context-menu/op-context-menu-handler.ts
+++ b/frontend/src/app/shared/components/op-context-menu/op-context-menu-handler.ts
@@ -1,3 +1,4 @@
+import { computePosition, ComputePositionReturn, flip, Placement, shift } from '@floating-ui/dom';
import { OPContextMenuService } from 'core-app/shared/components/op-context-menu/op-context-menu.service';
import { OpContextMenuItem } from 'core-app/shared/components/op-context-menu/op-context-menu.types';
import { UntilDestroyedMixin } from 'core-app/shared/helpers/angular/until-destroyed.mixin';
@@ -7,10 +8,12 @@ import { UntilDestroyedMixin } from 'core-app/shared/helpers/angular/until-destr
* This will often be a trigger component, but does not have to be.
*/
export abstract class OpContextMenuHandler extends UntilDestroyedMixin {
- protected $element:JQuery;
+ protected element:HTMLElement;
protected items:OpContextMenuItem[] = [];
+ protected placement:Placement = 'bottom-start';
+
constructor(readonly opContextMenu:OPContextMenuService) {
super();
}
@@ -22,26 +25,28 @@ export abstract class OpContextMenuHandler extends UntilDestroyedMixin {
*/
public onClose(focus = false) {
if (focus) {
- this.afterFocusOn.trigger('focus');
+ this.afterFocusOn.focus();
}
}
- public onOpen(menu:JQuery) {
- menu.find('.menu-item').first().trigger('focus');
+ public onOpen(menu:HTMLElement) {
+ menu.querySelector('.menu-item')?.focus();
}
/**
- * Positioning args for jquery-ui position.
+ * Compute position for Floating UI.
*
* @param {Event} openerEvent
*/
- public positionArgs(openerEvent:JQuery.TriggeredEvent|Event):JQueryUI.JQueryPositionOptions {
- return {
- my: 'left top',
- at: 'right bottom',
- of: openerEvent,
- collision: 'flipfit',
- };
+ public computePosition(floating:HTMLElement, openerEvent:Event):Promise {
+ const reference = openerEvent.target as HTMLElement;
+ return computePosition(reference, floating, {
+ placement: this.placement,
+ middleware: [
+ flip(),
+ shift({ padding: 10 }),
+ ],
+ });
}
/**
@@ -56,11 +61,11 @@ export abstract class OpContextMenuHandler extends UntilDestroyedMixin {
/**
* Open this context menu
*/
- protected open(evt:JQuery.TriggeredEvent):void {
+ protected open(evt:Event):void {
this.opContextMenu.show(this, evt);
}
- protected get afterFocusOn():JQuery {
- return this.$element;
+ protected get afterFocusOn():HTMLElement {
+ return this.element;
}
}
diff --git a/frontend/src/app/shared/components/op-context-menu/op-context-menu.service.ts b/frontend/src/app/shared/components/op-context-menu/op-context-menu.service.ts
index 734d9b887db9..0a4e6dd61620 100644
--- a/frontend/src/app/shared/components/op-context-menu/op-context-menu.service.ts
+++ b/frontend/src/app/shared/components/op-context-menu/op-context-menu.service.ts
@@ -21,6 +21,7 @@ export class OPContextMenuService {
// Allow temporarily disabling the close handler
private isOpening = false;
+ private openSeq = 0;
constructor(
readonly FocusHelper:FocusHelperService,
@@ -48,7 +49,7 @@ export class OPContextMenuService {
this.$transitions.onStart({}, () => this.close());
// Listen to keyups on window to close context menus
- jQuery(window).on('keydown', (evt:JQuery.TriggeredEvent) => {
+ window.addEventListener('keydown', (evt) => {
if (this.active && evt.key === 'Escape') {
this.close(true);
}
@@ -81,21 +82,48 @@ export class OPContextMenuService {
* @param component The context menu component to mount
*
*/
- public show(menu:OpContextMenuHandler, event:JQuery.TriggeredEvent|Event, component:ComponentType = OPContextMenuComponent):void {
+ public show(menu:OpContextMenuHandler, event:Event, component:ComponentType = OPContextMenuComponent):void {
this.close();
-
- // Create a portal for the given component class and render it
this.isOpening = true;
+ const seq = this.openSeq += 1;
+
+ // Create and attach portal
const portal = new ComponentPortal(component, null, this.injectorFor(menu.locals));
this.bodyPortalHost.attach(portal);
- this.portalHostElement.style.display = 'block';
+
+ // Avoid flicker until positioned
+ const hostEl = this.portalHostElement;
+ hostEl.style.visibility = 'hidden';
+ hostEl.style.display = 'block';
this.active = menu;
- setTimeout(() => {
- this.reposition(event);
- // Focus on the first element
- this.active?.onOpen(this.activeMenu);
- this.isOpening = false;
+ // Wait one frame to ensure component DOM exists, then position
+ requestAnimationFrame(() => {
+ if (!this.active || this.openSeq !== seq) {
+ this.isOpening = false;
+ return;
+ }
+
+ void this.reposition(event)
+ .then(() => {
+ if (this.active && this.openSeq === seq) {
+ hostEl.style.visibility = 'visible';
+ requestAnimationFrame(() => {
+ // Defer onOpen to next frame to ensure styles are applied
+ if (this.active && this.openSeq === seq) {
+ this.active.onOpen(this.activeMenu);
+ }
+ });
+ }
+ })
+ .catch((err) => {
+ // Fail-safe: close if positioning fails
+ console.error('Context menu positioning failed:', err);
+ if (this.openSeq === seq) this.close();
+ })
+ .finally(() => {
+ if (this.openSeq === seq) this.isOpening = false;
+ });
});
}
@@ -118,18 +146,24 @@ export class OPContextMenuService {
this.active = null;
}
- public reposition(event:JQuery.TriggeredEvent|Event):void {
+ public reposition(event:Event):Promise {
if (!this.active) {
- return;
+ return Promise.resolve();
}
- this.activeMenu
- .position(this.active.positionArgs(event))
- .css('visibility', 'visible');
+ return this.active.computePosition(this.activeMenu, event)
+ .then(({ x, y }) => {
+ Object.assign(this.activeMenu.style, {
+ left: `${x}px`,
+ top: `${y}px`,
+ position: 'absolute',
+ visibility: 'visible'
+ });
+ });
}
- public get activeMenu():JQuery {
- return jQuery(this.portalHostElement).find('.dropdown');
+ public get activeMenu():HTMLElement {
+ return this.portalHostElement.querySelector('.dropdown')!;
}
/**
diff --git a/frontend/src/app/shared/components/op-context-menu/op-context-menu.types.ts b/frontend/src/app/shared/components/op-context-menu/op-context-menu.types.ts
index 31b2027e01ab..077e413e5d4a 100644
--- a/frontend/src/app/shared/components/op-context-menu/op-context-menu.types.ts
+++ b/frontend/src/app/shared/components/op-context-menu/op-context-menu.types.ts
@@ -13,7 +13,7 @@ export interface OpContextMenuItem {
title?:string;
divider?:boolean;
isHeader?:boolean;
- onClick?:($event:JQuery.TriggeredEvent|MouseEvent) => boolean;
+ onClick?:(event:MouseEvent) => boolean;
}
export interface OpContextMenuLocalsMap {
diff --git a/frontend/src/app/shared/components/op-context-menu/wp-context-menu/wp-single-context-menu.ts b/frontend/src/app/shared/components/op-context-menu/wp-context-menu/wp-single-context-menu.ts
index b529ee7e6945..56cf2751564f 100644
--- a/frontend/src/app/shared/components/op-context-menu/wp-context-menu/wp-single-context-menu.ts
+++ b/frontend/src/app/shared/components/op-context-menu/wp-context-menu/wp-single-context-menu.ts
@@ -51,6 +51,8 @@ export class WorkPackageSingleContextMenuDirective extends OpContextMenuTrigger
private closeDialogHandler:EventListener = this.handleTimeEntryDialogClose.bind(this);
+ override readonly placement = 'bottom-end';
+
ngAfterViewInit():void {
super.ngAfterViewInit();
document.addEventListener('dialog:close', this.closeDialogHandler);
@@ -64,7 +66,7 @@ export class WorkPackageSingleContextMenuDirective extends OpContextMenuTrigger
document.removeEventListener('dialog:close', this.closeDialogHandler);
}
- protected open(evt:JQuery.TriggeredEvent) {
+ protected open(evt:Event) {
this.workPackage.project.$load().then(() => {
this.authorisationService.initModelAuth('work_package', this.workPackage.$links);
@@ -115,23 +117,6 @@ export class WorkPackageSingleContextMenuDirective extends OpContextMenuTrigger
}
}
- /**
- * Positioning args for jquery-ui position.
- *
- * @param {Event} openerEvent
- */
- public positionArgs(evt:JQuery.TriggeredEvent) {
- const additionalPositionArgs = {
- my: 'right top',
- at: 'right bottom',
- };
-
- const position = super.positionArgs(evt);
- _.assign(position, additionalPositionArgs);
-
- return position;
- }
-
private activeForWorkPackage(entry:TimeEntryResource|null):boolean {
return !!entry && entry.entity.href === this.workPackage.href;
}
@@ -196,9 +181,9 @@ export class WorkPackageSingleContextMenuDirective extends OpContextMenuTrigger
hidden: action.hidden === true,
linkText: I18n.t(`js.button_${key}`),
href: action.link,
- icon: action.icon ?? `icon-${key}`,
- onClick: ($event:JQuery.TriggeredEvent) => {
- if (action.link && isClickedWithModifier($event)) {
+ icon: action.icon || `icon-${key}`,
+ onClick: (event:MouseEvent) => {
+ if (action.link && isClickedWithModifier(event)) {
return false;
}
diff --git a/frontend/src/app/shared/components/op-context-menu/wp-context-menu/wp-table-context-menu.directive.ts b/frontend/src/app/shared/components/op-context-menu/wp-context-menu/wp-table-context-menu.directive.ts
index 9bb6fc04636e..08e49428471f 100644
--- a/frontend/src/app/shared/components/op-context-menu/wp-context-menu/wp-table-context-menu.directive.ts
+++ b/frontend/src/app/shared/components/op-context-menu/wp-context-menu/wp-table-context-menu.directive.ts
@@ -1,7 +1,7 @@
import { Injector } from '@angular/core';
import { WorkPackageAction } from 'core-app/features/work-packages/components/wp-table/context-menu-helper/wp-context-menu-helper.service';
import { WorkPackageTable } from 'core-app/features/work-packages/components/wp-fast-table/wp-fast-table';
-import { WorkPackageViewContextMenu } from 'core-app/shared/components/op-context-menu/wp-context-menu/wp-view-context-menu.directive';
+import { PositionArgs, WorkPackageViewContextMenu } from 'core-app/shared/components/op-context-menu/wp-context-menu/wp-view-context-menu.directive';
import { WorkPackageViewHierarchyIdentationService } from 'core-app/features/work-packages/routing/wp-view-base/view-services/wp-view-hierarchy-indentation.service';
import { InjectField } from 'core-app/shared/helpers/angular/inject-field.decorator';
@@ -10,10 +10,10 @@ export class WorkPackageTableContextMenu extends WorkPackageViewContextMenu {
constructor(public injector:Injector,
protected workPackageId:string,
- protected $element:JQuery,
- protected additionalPositionArgs:any = {},
+ protected element:HTMLElement,
+ additionalPositionArgs:PositionArgs,
protected table:WorkPackageTable) {
- super(injector, workPackageId, $element, additionalPositionArgs, true);
+ super(injector, workPackageId, element, additionalPositionArgs, true);
}
public triggerContextMenuAction(action:WorkPackageAction) {
diff --git a/frontend/src/app/shared/components/op-context-menu/wp-context-menu/wp-view-context-menu.directive.ts b/frontend/src/app/shared/components/op-context-menu/wp-context-menu/wp-view-context-menu.directive.ts
index 7501feb4b68a..80199e1eec4d 100644
--- a/frontend/src/app/shared/components/op-context-menu/wp-context-menu/wp-view-context-menu.directive.ts
+++ b/frontend/src/app/shared/components/op-context-menu/wp-context-menu/wp-view-context-menu.directive.ts
@@ -31,6 +31,10 @@ import { PathHelperService } from 'core-app/core/path-helper/path-helper.service
import { TurboRequestsService } from 'core-app/core/turbo/turbo-requests.service';
import { CurrentProjectService } from 'core-app/core/current-project/current-project.service';
+import { Placement } from '@floating-ui/dom';
+
+export interface PositionArgs { placement?:Placement, reference?:HTMLElement }
+
export class WorkPackageViewContextMenu extends OpContextMenuHandler {
@InjectField() protected states!:States;
@@ -67,15 +71,24 @@ export class WorkPackageViewContextMenu extends OpContextMenuHandler {
private copyToClipboardService:CopyToClipboardService;
+ protected reference:HTMLElement;
+
constructor(
public injector:Injector,
protected workPackageId:string,
- protected $element:JQuery,
- protected additionalPositionArgs:any = {},
+ protected element:HTMLElement,
+ additionalPositionArgs:PositionArgs = {},
protected allowSplitScreenActions:boolean = true,
) {
super(injector.get(OPContextMenuService));
this.copyToClipboardService = injector.get(CopyToClipboardService);
+
+ if (typeof additionalPositionArgs.placement !== 'undefined') {
+ this.placement = additionalPositionArgs.placement;
+ }
+ if (typeof additionalPositionArgs.reference !== 'undefined') {
+ this.reference = additionalPositionArgs.reference;
+ }
}
public get locals():OpContextMenuLocalsMap {
@@ -86,13 +99,6 @@ export class WorkPackageViewContextMenu extends OpContextMenuHandler {
};
}
- public positionArgs(evt:JQuery.TriggeredEvent) {
- const position = super.positionArgs(evt);
- _.assign(position, this.additionalPositionArgs);
-
- return position;
- }
-
public triggerContextMenuAction(action:WorkPackageAction) {
const { link } = action;
const id = this.workPackage.id as string;
@@ -196,8 +202,8 @@ export class WorkPackageViewContextMenu extends OpContextMenuHandler {
linkText: action.text,
href: action.href,
icon: action.icon != null ? action.icon : `icon-${action.key}`,
- onClick: ($event:JQuery.TriggeredEvent) => {
- if (action.href && isClickedWithModifier($event)) {
+ onClick: (event:MouseEvent) => {
+ if (action.href && isClickedWithModifier(event)) {
return false;
}
@@ -216,8 +222,8 @@ export class WorkPackageViewContextMenu extends OpContextMenuHandler {
class: 'openFullScreenView',
href: link,
linkText: I18n.t('js.button_open_fullscreen'),
- onClick: ($event:JQuery.TriggeredEvent) => {
- if (isClickedWithModifier($event)) {
+ onClick: (event) => {
+ if (isClickedWithModifier(event)) {
return false;
}
@@ -237,8 +243,8 @@ export class WorkPackageViewContextMenu extends OpContextMenuHandler {
{ workPackageId: this.workPackageId, tabIdentifier: 'overview' },
),
linkText: I18n.t('js.button_open_details'),
- onClick: ($event:JQuery.TriggeredEvent) => {
- if (isClickedWithModifier($event)) {
+ onClick: (event) => {
+ if (isClickedWithModifier(event)) {
return false;
}
diff --git a/frontend/src/app/shared/components/persistent-toggle/persistent-toggle.component.ts b/frontend/src/app/shared/components/persistent-toggle/persistent-toggle.component.ts
index 98e578437587..3ac45db30839 100644
--- a/frontend/src/app/shared/components/persistent-toggle/persistent-toggle.component.ts
+++ b/frontend/src/app/shared/components/persistent-toggle/persistent-toggle.component.ts
@@ -27,6 +27,7 @@
//++
import { Component, ElementRef, OnInit } from '@angular/core';
+import { slideDown, slideUp } from 'es6-slide-up-down';
@Component({
@@ -42,53 +43,56 @@ export class PersistentToggleComponent implements OnInit {
private isHidden = false;
/** Element reference */
- private $element:JQuery;
+ private element:HTMLElement;
- private $targetNotification:JQuery;
+ private targetNotification:HTMLElement|null;
- constructor(private elementRef:ElementRef) {
+ constructor(private elementRef:ElementRef) {
}
ngOnInit():void {
- this.$element = jQuery(this.elementRef.nativeElement);
- this.$targetNotification = this.getTargetNotification();
+ this.element = this.elementRef.nativeElement;
+ this.targetNotification = this.getTargetNotification();
- this.identifier = this.$element.data('identifier');
+ this.identifier = this.element.dataset.identifier!;
this.isHidden = window.OpenProject.guardedLocalStorage(this.identifier) === 'true';
// Set initial state
- this.$targetNotification.prop('hidden', !!this.isHidden);
-
- // Register click handler
- this.$element
- .parent()
- .find('.persistent-toggle--click-handler')
- .on('click', () => this.toggle(!this.isHidden));
-
- // Register target toaster close icon
- this.$targetNotification
- .find('.op-toast--close')
- .on('click', () => this.toggle(true));
+ if (this.targetNotification) {
+ this.targetNotification.hidden = !!this.isHidden;
+
+ // Register click handler
+ this.element
+ .parentElement
+ ?.querySelector('.persistent-toggle--click-handler')
+ ?.addEventListener('click', () => this.toggle(!this.isHidden));
+
+ // Register target toaster close icon
+ this.targetNotification
+ .querySelector('.op-toast--close')
+ ?.addEventListener('click', () => this.toggle(true));
+ }
}
private getTargetNotification() {
- return this.$element
- .parent()
- .find('.persistent-toggle--toaster');
+ return this.element
+ .parentElement!
+ .querySelector('.persistent-toggle--toaster');
}
private toggle(isNowHidden:boolean) {
this.isHidden = isNowHidden;
window.OpenProject.guardedLocalStorage(this.identifier, (!!isNowHidden).toString());
+ const targetNotification = this.targetNotification;
+ if (!targetNotification) return;
+
if (isNowHidden) {
- this.$targetNotification.slideUp(400, () => {
- // Set hidden only after animation completed
- this.$targetNotification.prop('hidden', true);
- });
+ slideUp(targetNotification, 400);
+ setTimeout(() => { targetNotification.hidden = true; }, 400);
} else {
- this.$targetNotification.slideDown(400);
- this.$targetNotification.prop('hidden', false);
+ targetNotification.hidden = false;
+ slideDown(targetNotification, 400);
}
}
}
diff --git a/frontend/src/app/shared/components/project-include/list/project-include-list.component.ts b/frontend/src/app/shared/components/project-include/list/project-include-list.component.ts
index d2cdd5a62eef..2e9a0c259bd6 100644
--- a/frontend/src/app/shared/components/project-include/list/project-include-list.component.ts
+++ b/frontend/src/app/shared/components/project-include/list/project-include-list.component.ts
@@ -43,6 +43,7 @@ import { PathHelperService } from 'core-app/core/path-helper/path-helper.service
import {
SearchableProjectListService,
} from 'core-app/shared/components/searchable-project-list/searchable-project-list.service';
+import { getMetaContent } from 'core-app/core/setup/globals/global-helpers';
@Component({
// eslint-disable-next-line @angular-eslint/component-selector
@@ -123,13 +124,13 @@ export class OpProjectIncludeListComponent {
}
extendedProjectUrl(projectId:string):string {
- const currentMenuItem = document.querySelector('meta[name="current_menu_item"]') as HTMLMetaElement;
+ const currentMenuItem = getMetaContent('current_menu_item');
const url = this.pathHelper.projectPath(projectId);
if (!currentMenuItem) {
return url;
}
- return `${url}?jump=${encodeURIComponent(currentMenuItem.content)}`;
+ return `${url}?jump=${encodeURIComponent(currentMenuItem)}`;
}
}
diff --git a/frontend/src/app/shared/components/resizer/resizer/main-menu-resizer.component.ts b/frontend/src/app/shared/components/resizer/resizer/main-menu-resizer.component.ts
index 210a10ababd5..f3ffaa646e22 100644
--- a/frontend/src/app/shared/components/resizer/resizer/main-menu-resizer.component.ts
+++ b/frontend/src/app/shared/components/resizer/resizer/main-menu-resizer.component.ts
@@ -66,7 +66,7 @@ export class MainMenuResizerComponent extends UntilDestroyedMixin implements OnI
private elementWidth:number;
- private mainMenu = jQuery('#main-menu')[0];
+ private mainMenu = document.querySelector('#main-menu')!;
public moving = false;
diff --git a/frontend/src/app/shared/components/resizer/resizer/wp-resizer.component.ts b/frontend/src/app/shared/components/resizer/resizer/wp-resizer.component.ts
index 6df43ba8a610..f11a230beded 100644
--- a/frontend/src/app/shared/components/resizer/resizer/wp-resizer.component.ts
+++ b/frontend/src/app/shared/components/resizer/resizer/wp-resizer.component.ts
@@ -190,7 +190,7 @@ export class WpResizerComponent extends UntilDestroyedMixin implements OnInit, A
private applyColumnLayout(checkWidth = 750) {
const singleView = document.querySelector("[data-selector='wp-single-view']");
if (singleView) {
- jQuery(singleView).toggleClass('work-package--single-view_with-columns', singleView.offsetWidth > checkWidth);
+ singleView.classList.toggle('work-package--single-view_with-columns', singleView.offsetWidth > checkWidth);
}
}
diff --git a/frontend/src/app/shared/components/tabs/scrollable-tabs/scrollable-tabs.component.ts b/frontend/src/app/shared/components/tabs/scrollable-tabs/scrollable-tabs.component.ts
index 7141da788995..deb22ce3453e 100644
--- a/frontend/src/app/shared/components/tabs/scrollable-tabs/scrollable-tabs.component.ts
+++ b/frontend/src/app/shared/components/tabs/scrollable-tabs/scrollable-tabs.component.ts
@@ -192,13 +192,23 @@ export class ScrollableTabsComponent extends UntilDestroyedMixin implements Afte
}
private scrollIntoVisibleArea(tabId:string) {
- const tab:JQuery = jQuery(this.pane).find(`[data-tab-id=${tabId}]`);
- const position:JQueryCoordinates = tab.position();
-
- const tabRightBorderAt:number = position.left + Number(tab.outerWidth());
+ const tab = this.pane.querySelector(`[data-tab-id=${tabId}]`)!;
+ const position = getPosition(tab);
+ const tabRightBorderAt = position.left + tab.offsetWidth;
if (this.pane.scrollLeft + this.container.clientWidth < tabRightBorderAt) {
this.pane.scrollLeft = tabRightBorderAt - this.container.clientWidth + 40; // 40px to not overlap by buttons
}
}
}
+
+function getPosition(el:HTMLElement) {
+ const offsetParent = el.offsetParent || document.body;
+ const elRect = el.getBoundingClientRect();
+ const parentRect = offsetParent.getBoundingClientRect();
+
+ return {
+ top: elRect.top - parentRect.top - parseFloat(getComputedStyle(offsetParent).borderTopWidth),
+ left: elRect.left - parentRect.left - parseFloat(getComputedStyle(offsetParent).borderLeftWidth)
+ };
+}
diff --git a/frontend/src/app/shared/components/toaster/toast.service.ts b/frontend/src/app/shared/components/toaster/toast.service.ts
index 5b1fff5448e6..c1e6192c713b 100644
--- a/frontend/src/app/shared/components/toaster/toast.service.ts
+++ b/frontend/src/app/shared/components/toaster/toast.service.ts
@@ -38,7 +38,7 @@ import waitForUploadsFinished from 'core-app/core/upload/wait-for-uploads-finish
import { IHalErrorBase, IHalMultipleError, isHalError } from 'core-app/features/hal/resources/error-resource';
export function removeSuccessFlashMessages():void {
- jQuery('.op-toast.-success').remove();
+ document.querySelectorAll('.op-toast.-success').forEach((flashMessage) => flashMessage.remove());
}
export type ToastType = 'success'|'error'|'warning'|'info'|'upload'|'loading';
@@ -61,10 +61,9 @@ export class ToastService {
readonly configurationService:ConfigurationService,
readonly I18n:I18nService,
) {
- jQuery(window).on(
- OPToastEvent,
- (event:JQuery.TriggeredEvent, toast:IToast) => { this.add(toast); },
- );
+ window.addEventListener(OPToastEvent, ({ detail:toast }:CustomEvent) => {
+ this.add(toast);
+ });
}
/**
diff --git a/frontend/src/app/shared/components/work-package-graphs/configuration-modal/wp-graph-configuration.modal.ts b/frontend/src/app/shared/components/work-package-graphs/configuration-modal/wp-graph-configuration.modal.ts
index 50e3a0b41c2d..d7b4b9d6ad8a 100644
--- a/frontend/src/app/shared/components/work-package-graphs/configuration-modal/wp-graph-configuration.modal.ts
+++ b/frontend/src/app/shared/components/work-package-graphs/configuration-modal/wp-graph-configuration.modal.ts
@@ -38,7 +38,7 @@ export const WpTableConfigurationModalPrependToken = new InjectionToken {
@@ -130,6 +130,6 @@ export class WpGraphConfigurationModalComponent extends OpModalComponent impleme
}
protected get afterFocusOn():HTMLElement {
- return this.$element;
+ return this.element;
}
}
diff --git a/frontend/src/app/shared/directives/a11y/keyboard-shortcut.service.ts b/frontend/src/app/shared/directives/a11y/keyboard-shortcut.service.ts
index 0439e88b8f59..707ced364cdc 100644
--- a/frontend/src/app/shared/directives/a11y/keyboard-shortcut.service.ts
+++ b/frontend/src/app/shared/directives/a11y/keyboard-shortcut.service.ts
@@ -97,15 +97,15 @@ export class KeyboardShortcutService {
const key = accessKeys[keyName];
return () => {
- const elem = jQuery(`[accesskey=${key}]:first`);
- if (elem.is('input') || elem.attr('id') === 'global-search-input') {
+ const elem = document.querySelector(`[accesskey="${key}"]`)!;
+ if (elem instanceof HTMLInputElement || elem.getAttribute('id') === 'global-search-input') {
// timeout with delay so that the key is not
// triggered on the input
- setTimeout(() => this.FocusHelper.focus(elem[0]), 200);
- } else if (elem.is('[href]')) {
- this.clickLink(elem[0] as HTMLLinkElement);
+ setTimeout(() => this.FocusHelper.focus(elem), 200);
+ } else if (elem instanceof HTMLAnchorElement) {
+ this.clickLink(elem);
} else {
- elem[0].click();
+ elem.click();
}
};
}
@@ -126,7 +126,7 @@ export class KeyboardShortcutService {
}
// eslint-disable-next-line class-methods-use-this
- clickLink(link:HTMLLinkElement):void {
+ clickLink(link:HTMLAnchorElement):void {
const event = new MouseEvent('click', {
view: window,
bubbles: true,
diff --git a/frontend/src/app/shared/directives/focus/focus-within.directive.ts b/frontend/src/app/shared/directives/focus/focus-within.directive.ts
index 5f4b562f2bb5..2e54670550bc 100644
--- a/frontend/src/app/shared/directives/focus/focus-within.directive.ts
+++ b/frontend/src/app/shared/directives/focus/focus-within.directive.ts
@@ -42,12 +42,12 @@ import { UntilDestroyedMixin } from 'core-app/shared/helpers/angular/until-destr
export class FocusWithinDirective extends UntilDestroyedMixin implements OnInit {
@Input() public selector:string;
- constructor(readonly elementRef:ElementRef) {
+ constructor(readonly elementRef:ElementRef) {
super();
}
ngOnInit() {
- const element = jQuery(this.elementRef.nativeElement);
+ const element = this.elementRef.nativeElement;
const focusedObservable = new BehaviorSubject(false);
focusedObservable
@@ -56,22 +56,22 @@ export class FocusWithinDirective extends UntilDestroyedMixin implements OnInit
auditTime(50),
)
.subscribe((focused) => {
- element.toggleClass('-focus', focused);
+ element.classList.toggle('-focus', focused);
});
const focusListener = function () {
focusedObservable.next(true);
};
- element[0].addEventListener('focus', focusListener, true);
+ element.addEventListener('focus', focusListener, true);
const blurListener = function () {
focusedObservable.next(false);
};
- element[0].addEventListener('blur', blurListener, true);
+ element.addEventListener('blur', blurListener, true);
setTimeout(() => {
- element.addClass('op-focus-within');
- element.find(this.selector).addClass('op-focus-within');
+ element.classList.add('op-focus-within');
+ element.querySelector(this.selector)?.classList.add('op-focus-within');
}, 0);
}
}
diff --git a/frontend/src/app/shared/directives/op-drag-scroll/op-drag-scroll.directive.ts b/frontend/src/app/shared/directives/op-drag-scroll/op-drag-scroll.directive.ts
index 5e501bd84af2..31e7ca11b93d 100644
--- a/frontend/src/app/shared/directives/op-drag-scroll/op-drag-scroll.directive.ts
+++ b/frontend/src/app/shared/directives/op-drag-scroll/op-drag-scroll.directive.ts
@@ -27,17 +27,22 @@
//++
import { Directive, ElementRef, OnInit } from '@angular/core';
+declare global {
+ interface GlobalEventHandlersEventMap {
+ 'op:dragscroll': CustomEvent<{x: number, y:number}>;
+ }
+}
+
@Directive({
selector: 'op-drag-scroll',
standalone: false,
})
export class OpDragScrollDirective implements OnInit {
- constructor(readonly elementRef:ElementRef) {
+ constructor(readonly elementRef:ElementRef) {
}
ngOnInit() {
- const element = jQuery(this.elementRef.nativeElement);
- const eventName = 'op:dragscroll';
+ const element = this.elementRef.nativeElement;
// Is mouse down?
let mousedown = false;
@@ -47,7 +52,7 @@ export class OpDragScrollDirective implements OnInit {
mousedownY:number;
// Mousedown: Potential drag start
- element.on('mousedown', (evt) => {
+ element.addEventListener('mousedown', (evt) => {
setTimeout(() => {
mousedown = true;
mousedownX = evt.clientX;
@@ -56,21 +61,25 @@ export class OpDragScrollDirective implements OnInit {
});
// Mouseup: Potential drag stop
- element.on('mouseup', () => {
+ element.addEventListener('mouseup', () => {
mousedown = false;
});
// Mousemove: Report movement if mousedown
- element.on('mousemove', (evt) => {
+ element.addEventListener('mousemove', (evt) => {
if (!mousedown) {
return;
}
// Trigger drag scroll event
- element.trigger(eventName, {
- x: evt.clientX - mousedownX,
- y: evt.clientY - mousedownY,
- });
+ element.dispatchEvent(
+ new CustomEvent('op:dragscroll', {
+ detail: {
+ x: evt.clientX - mousedownX,
+ y: evt.clientY - mousedownY,
+ },
+ }),
+ );
// Update last mouse position
mousedownX = evt.clientX;
diff --git a/frontend/src/app/shared/helpers/dom-helpers.ts b/frontend/src/app/shared/helpers/dom-helpers.ts
index 477191f98436..e88c5838d5a5 100644
--- a/frontend/src/app/shared/helpers/dom-helpers.ts
+++ b/frontend/src/app/shared/helpers/dom-helpers.ts
@@ -26,6 +26,8 @@
// See COPYRIGHT and LICENSE files for more details.
//++
+export const getNodeIndex = (element:Element) => Array.from(element.parentNode!.children).indexOf(element);
+
/**
* Toggles the visibility of an HTMLElement using `hidden` property.
*
@@ -41,6 +43,10 @@ export function toggleElement(element:HTMLElement, value?:boolean) {
}
};
+export const showElement = (element:HTMLElement) => toggleElement(element, true);
+
+export const hideElement = (element:HTMLElement) => toggleElement(element, false);
+
/**
* Toggles the visibility of an Element using a CSS class.
* Also takes care of setting `aria-hidden` attribute for accessibility.
@@ -69,3 +75,24 @@ export function toggleElementByVisibility(element:HTMLElement, value?:boolean) {
value ??= element.style.getPropertyValue('visibility') !== 'visible';
element.style.setProperty('visibility', value ? 'visible' : 'hidden');
};
+
+/**
+ * Mimics jQuery(':visible')
+ */
+export function isVisible(elem:HTMLElement|null) {
+ if (!elem) return false;
+
+ // Check if element is in the DOM
+ if (!document.contains(elem)) return false;
+
+ // Check if dimensions are visible
+ return !!(
+ elem.offsetWidth
+ || elem.offsetHeight
+ || elem.getClientRects().length
+ );
+}
+
+export function queryVisible(selector:string, node:Element|Document = document) {
+ return Array.from(node.querySelectorAll(selector)).filter(isVisible);
+}
diff --git a/frontend/src/app/shared/helpers/drag-and-drop/dom-autoscroll.service.ts b/frontend/src/app/shared/helpers/drag-and-drop/dom-autoscroll.service.ts
index 30a30ca318d6..d1a07dd0d978 100644
--- a/frontend/src/app/shared/helpers/drag-and-drop/dom-autoscroll.service.ts
+++ b/frontend/src/app/shared/helpers/drag-and-drop/dom-autoscroll.service.ts
@@ -27,10 +27,11 @@ export class DomAutoscrollService {
public pointCB:any;
+ private abortController:AbortController;
+
constructor(elements:Element[],
params:any) {
this.elements = elements;
- this.scrollWhenOutside = params.scrollWhenOutside || false;
this.maxSpeed = params.maxSpeed || 5;
this.margin = params.margin || 10;
this.scrollWhenOutside = params.scrollWhenOutside || false;
@@ -42,20 +43,29 @@ export class DomAutoscrollService {
}
public init() {
- jQuery(window).on('mousemove.domautoscroll touchmove.domautoscroll', (evt:any) => {
+ this.abortController = new AbortController();
+ const { signal } = this.abortController;
+ const moveHandler = (evt:Event) => {
if (this.down) {
this.pointCB(evt);
this.onMove(evt);
}
- });
- jQuery(window).on('mousedown.domautoscroll touchstart.domautoscroll', () => { this.down = true; });
- jQuery(window).on('mouseup.domautoscroll touchend.domautoscroll', () => this.onUp());
- jQuery(window).on('scroll.domautoscroll', (evt:any) => this.setScroll(evt));
+ };
+ const downHandler = () => { this.down = true; };
+ const upHandler = () => { this.onUp(); };
+ const scrollHandler = (evt:Event) => { this.setScroll(evt); };
+
+ window.addEventListener('mousemove', moveHandler, { signal });
+ window.addEventListener('touchmove', moveHandler, { signal });
+ window.addEventListener('mousedown', downHandler, { signal });
+ window.addEventListener('touchstart', downHandler, { signal });
+ window.addEventListener('mouseup', upHandler, { signal });
+ window.addEventListener('touchend', upHandler, { signal });
+ window.addEventListener('scroll', scrollHandler, { signal });
}
public destroy() {
- jQuery(window).off('.domautoscroll');
-
+ this.abortController.abort();
this.elements = [];
this.cleanAnimation();
}
diff --git a/frontend/src/app/shared/helpers/event-helpers.ts b/frontend/src/app/shared/helpers/event-helpers.ts
new file mode 100644
index 000000000000..db3e4035939d
--- /dev/null
+++ b/frontend/src/app/shared/helpers/event-helpers.ts
@@ -0,0 +1,149 @@
+//-- copyright
+// OpenProject is an open source project management software.
+// Copyright (C) the OpenProject GmbH
+//
+// This program is free software; you can redistribute it and/or
+// modify it under the terms of the GNU General Public License version 3.
+//
+// OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
+// Copyright (C) 2006-2013 Jean-Philippe Lang
+// Copyright (C) 2010-2013 the ChiliProject Team
+//
+// This program is free software; you can redistribute it and/or
+// modify it under the terms of the GNU General Public License
+// as published by the Free Software Foundation; either version 2
+// of the License, or (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program; if not, write to the Free Software
+// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+//
+// See COPYRIGHT and LICENSE files for more details.
+//++
+
+const eventTargetCache = new WeakMap();
+
+type EventType = keyof HTMLElementEventMap;
+type EventNamespace = string;
+type NamespacedEvent = `${EventType}.${EventNamespace}`;
+type NamespacedEvents = `.${EventNamespace}`;
+type EventListeners = EventListenerOrEventListenerObject[];
+
+class EventListenerRegistry {
+ private listenerMap = new Map>();
+
+ constructor(private eventTarget:EventTarget) {}
+
+ on(
+ namespacedEvent:NamespacedEvent,
+ handler:EventListenerOrEventListenerObject,
+ options?:AddEventListenerOptions|boolean
+ ) {
+ const [namespace, type] = this.getNamespaceAndType(namespacedEvent);
+
+ this.listenerMap.set(namespace, this.listenerMap.get(namespace) ?? new Map());
+ const listenersForNamespace = this.listenerMap.get(namespace)!;
+
+ if (!type) return;
+
+ if (!listenersForNamespace.has(type)) {
+ listenersForNamespace.set(type, [handler]);
+ } else {
+ const existingListeners = listenersForNamespace.get(type)!;
+ if (!existingListeners.includes(handler)) {
+ existingListeners.push(handler);
+ }
+ }
+
+ this.eventTarget.addEventListener(type, handler, options);
+ }
+
+ one(
+ namespacedEvent:NamespacedEvent,
+ handler:EventListenerOrEventListenerObject,
+ options?:AddEventListenerOptions
+ ) {
+ const wrappedHandler:EventListener = (event) => {
+ this.removeHandlerFromRegistry(namespacedEvent, wrappedHandler);
+
+ if (typeof handler === 'function') {
+ handler(event);
+ } else {
+ handler.handleEvent(event);
+ }
+ };
+
+ this.on(namespacedEvent, wrappedHandler, { ...options, once: true });
+ }
+
+ off(namespacedEvent:NamespacedEvent|NamespacedEvents) {
+ const [namespace, type] = this.getNamespaceAndType(namespacedEvent);
+ const listenersForNamespace = this.listenerMap.get(namespace);
+ if (!listenersForNamespace) return;
+
+ if (type) {
+ const listeners = listenersForNamespace.get(type) ?? [];
+ listeners.forEach((listener) => {
+ this.eventTarget.removeEventListener(type, listener);
+ });
+ listenersForNamespace.delete(type);
+ } else {
+ listenersForNamespace.forEach((listeners, eventType) => {
+ listeners.forEach((listener) => {
+ this.eventTarget.removeEventListener(eventType, listener);
+ });
+ });
+ this.listenerMap.delete(namespace);
+ }
+
+ // Clean up empty namespace
+ if (listenersForNamespace.size === 0) {
+ this.listenerMap.delete(namespace);
+ }
+ }
+
+ private removeHandlerFromRegistry(namespacedEvent:NamespacedEvent, handler:EventListenerOrEventListenerObject) {
+ const [namespace, type] = this.getNamespaceAndType(namespacedEvent);
+ if (!namespace) return;
+
+ const listenersForNamespace = this.listenerMap.get(namespace);
+ if (!listenersForNamespace) return;
+
+ if (!type) return;
+
+ const listeners = listenersForNamespace.get(type);
+ if (!listeners) return;
+
+ const index = listeners.indexOf(handler);
+ if (index > -1) {
+ listeners.splice(index, 1);
+ }
+
+ if (listeners.length === 0) {
+ listenersForNamespace.delete(type);
+ }
+ if (listenersForNamespace.size === 0) {
+ this.listenerMap.delete(namespace);
+ }
+ }
+
+ private getNamespaceAndType(namespacedEvent:NamespacedEvent|NamespacedEvents):[EventNamespace, EventType|null] {
+ const parts = namespacedEvent.split('.').reverse();
+ const namespace = parts[0];
+ const type = (parts[1] as EventType | undefined) ?? null;
+ return [namespace, type];
+ }
+}
+
+export function target(eventTarget:EventTarget):EventListenerRegistry {
+ if (!eventTargetCache.has(eventTarget)) {
+ eventTargetCache.set(eventTarget, new EventListenerRegistry(eventTarget));
+ }
+
+ return eventTargetCache.get(eventTarget)!;
+}
diff --git a/frontend/src/app/shared/helpers/link-handling/link-handling.ts b/frontend/src/app/shared/helpers/link-handling/link-handling.ts
index 1956b7ad0bb8..f4a54e393c0f 100644
--- a/frontend/src/app/shared/helpers/link-handling/link-handling.ts
+++ b/frontend/src/app/shared/helpers/link-handling/link-handling.ts
@@ -26,7 +26,7 @@
// See COPYRIGHT and LICENSE files for more details.
//++
-export function isClickedWithModifier(event:MouseEvent|JQuery.TriggeredEvent) {
+export function isClickedWithModifier(event:MouseEvent) {
const modifier = event.ctrlKey || event.shiftKey || event.metaKey;
const middleButton = event.button === 1;
diff --git a/frontend/src/app/shared/helpers/set-click-position/set-click-position.ts b/frontend/src/app/shared/helpers/set-click-position/set-click-position.ts
index a4eb7dd2080d..2d4bb7636604 100644
--- a/frontend/src/app/shared/helpers/set-click-position/set-click-position.ts
+++ b/frontend/src/app/shared/helpers/set-click-position/set-click-position.ts
@@ -20,20 +20,18 @@ export function setPosition(element:HTMLInputElement, offset:number):void {
* @param evt
* @return {number}
*/
-export function getPosition(evt:any):number {
- const originalEvt = evt.originalEvent;
-
+export function getPosition(evt:MouseEvent):number {
try {
- if (originalEvt.rangeParent) {
+ if ((evt as any).rangeParent) {
const range = document.createRange();
- range.setStart(originalEvt.rangeParent, originalEvt.rangeOffset);
+ range.setStart((evt as any).rangeParent, (evt as any).rangeOffset);
return range.startOffset;
}
const legacyDocument = document as { caretRangeFromPoint?:(x:number, y:number) => { startOffset:number } };
if (legacyDocument.caretRangeFromPoint) {
return legacyDocument
- .caretRangeFromPoint((evt as MouseEvent).clientX, (evt as MouseEvent).clientY)
+ .caretRangeFromPoint(evt.clientX, evt.clientY)
.startOffset;
}
diff --git a/frontend/src/global_styles/layout/work_packages/_table.sass b/frontend/src/global_styles/layout/work_packages/_table.sass
index 920b12bc05ab..3e7273eeb144 100644
--- a/frontend/src/global_styles/layout/work_packages/_table.sass
+++ b/frontend/src/global_styles/layout/work_packages/_table.sass
@@ -139,6 +139,9 @@ body[class*="router--"]
// Hinter browser that the content of the flex is contained except for size
contain: strict
+ &.-timeline-visible
+ display: block
+
.router--work-packages-base
.work-packages-partitioned-page--content-left
overflow: hidden
diff --git a/frontend/src/main.ts b/frontend/src/main.ts
index 726046c40755..7e2649771f14 100644
--- a/frontend/src/main.ts
+++ b/frontend/src/main.ts
@@ -41,11 +41,7 @@ if (environment.production) {
// Import the correct locale early on
void initializeLocale()
.then(() => {
- jQuery(() => {
- // Now that DOM is loaded, also run the global listeners
- initializeGlobalListeners();
+ initializeGlobalListeners();
- // Due to the behaviour of the Edge browser we need to wait for 'DOM ready'
- void platformBrowserDynamic().bootstrapModule(OpenProjectModule);
- });
+ void platformBrowserDynamic().bootstrapModule(OpenProjectModule);
});
diff --git a/frontend/src/stimulus/controllers/select-autosize.controller.ts b/frontend/src/stimulus/controllers/select-autosize.controller.ts
new file mode 100644
index 000000000000..ec925f0f7c89
--- /dev/null
+++ b/frontend/src/stimulus/controllers/select-autosize.controller.ts
@@ -0,0 +1,63 @@
+//-- copyright
+// OpenProject is an open source project management software.
+// Copyright (C) the OpenProject GmbH
+//
+// This program is free software; you can redistribute it and/or
+// modify it under the terms of the GNU General Public License version 3.
+//
+// OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
+// Copyright (C) 2006-2013 Jean-Philippe Lang
+// Copyright (C) 2010-2013 the ChiliProject Team
+//
+// This program is free software; you can redistribute it and/or
+// modify it under the terms of the GNU General Public License
+// as published by the Free Software Foundation; either version 2
+// of the License, or (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program; if not, write to the Free Software
+// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+//
+// See COPYRIGHT and LICENSE files for more details.
+//++
+
+import { Controller } from '@hotwired/stimulus';
+import { useDebounce, useMutation } from 'stimulus-use';
+
+/**
+ * A simple controller to allow elements automatically grow to fit
+ * their options.
+ *
+ * Use `data-select-autosize-size-limit-value` to specify a size limit.
+ */
+export default class SelectAutosizeController extends Controller {
+ static values = {
+ sizeLimit: { type: Number, default: 10 }
+ };
+
+ static debounces = ['updateSize'];
+
+ declare sizeLimitValue:number;
+
+ connect() {
+ useMutation(this, { childList: true });
+ useDebounce(this, { wait: 100 });
+
+ this.updateSize();
+ }
+
+ mutate(mutations:MutationRecord[]) {
+ if (mutations.some(m => m.type === 'childList')) {
+ this.updateSize();
+ }
+ }
+
+ private updateSize() {
+ this.element.size = Math.min(this.element.options.length, this.sizeLimitValue);
+ }
+}
diff --git a/frontend/src/stimulus/helpers/request-helpers.ts b/frontend/src/stimulus/helpers/request-helpers.ts
index eb19244765dd..2357857196f5 100644
--- a/frontend/src/stimulus/helpers/request-helpers.ts
+++ b/frontend/src/stimulus/helpers/request-helpers.ts
@@ -29,6 +29,8 @@
*/
import { FetchRequest, FetchResponse, Options } from '@rails/request.js';
+import { hideElement, showElement } from 'core-app/shared/helpers/dom-helpers';
+import invariant from 'tiny-invariant';
export function post(url:string|URL, options?:Options) {
const request = new FetchRequest('post', url, options);
@@ -36,10 +38,12 @@ export function post(url:string|URL, options?:Options) {
}
function withAjaxIndicator(request:Promise) {
- jQuery('#ajax-indicator').show();
+ const ajaxIndicator = document.querySelector('#ajax-indicator');
+ invariant(ajaxIndicator, 'Expected an Element with id ajax-indicator to be present');
+ showElement(ajaxIndicator);
return request.then((response) => {
- jQuery('#ajax-indicator').hide();
+ hideElement(ajaxIndicator);
return response;
});
}
diff --git a/frontend/src/stimulus/setup.ts b/frontend/src/stimulus/setup.ts
index fcf37f02730e..3a336266fd1c 100644
--- a/frontend/src/stimulus/setup.ts
+++ b/frontend/src/stimulus/setup.ts
@@ -34,6 +34,7 @@ import { BeforeunloadController } from './controllers/beforeunload.controller';
import ExternalLinksController from './controllers/external-links.controller';
import DisableWhenClickedController from 'core-stimulus/controllers/disable-when-clicked.controller';
import HighlightTargetElementController from 'core-stimulus/controllers/highlight-target-element.controller';
+import SelectAutosizeController from 'core-stimulus/controllers/select-autosize.controller';
declare global {
interface Window {
@@ -73,6 +74,7 @@ OpenProjectStimulusApplication.preregister('beforeunload', BeforeunloadControlle
OpenProjectStimulusApplication.preregister('auto-theme-switcher', AutoThemeSwitcher);
OpenProjectStimulusApplication.preregister('external-links', ExternalLinksController);
OpenProjectStimulusApplication.preregister('highlight-target-element', HighlightTargetElementController);
+OpenProjectStimulusApplication.preregister('select-autosize', SelectAutosizeController);
const instance = OpenProjectStimulusApplication.start();
window.Stimulus = instance;
diff --git a/frontend/src/turbo/turbo-global-listeners.ts b/frontend/src/turbo/turbo-global-listeners.ts
index 5fdd94747019..42423f4aa7da 100644
--- a/frontend/src/turbo/turbo-global-listeners.ts
+++ b/frontend/src/turbo/turbo-global-listeners.ts
@@ -38,8 +38,8 @@ export function addTurboGlobalListeners() {
//
// Action menu logic
- jQuery('.toolbar-items').each((_, menu:HTMLElement) => {
- installMenuLogic(jQuery(menu));
+ document.querySelectorAll('.toolbar-items').forEach((menu) => {
+ installMenuLogic(menu);
});
// Legacy settings listener
diff --git a/frontend/src/typings/open-project.typings.d.ts b/frontend/src/typings/open-project.typings.d.ts
index 5fb533b424f4..be332575236a 100644
--- a/frontend/src/typings/open-project.typings.d.ts
+++ b/frontend/src/typings/open-project.typings.d.ts
@@ -74,11 +74,6 @@ interface Function {
_type:string;
}
-interface JQuery {
- topShelf:any;
- mark:any;
-}
-
declare let Factory:any;
declare namespace op {
diff --git a/frontend/src/typings/shims.d.ts b/frontend/src/typings/shims.d.ts
index 1f86c67d86ae..0e5a136b6bd3 100644
--- a/frontend/src/typings/shims.d.ts
+++ b/frontend/src/typings/shims.d.ts
@@ -70,8 +70,6 @@ declare global {
}
interface JQuery {
- topShelf:any;
- mark:any;
tablesorter:any;
}
diff --git a/modules/backlogs/app/views/shared/_server_variables.js.erb b/modules/backlogs/app/views/shared/_server_variables.js.erb
index 6215bb0a8702..d03965ee336b 100644
--- a/modules/backlogs/app/views/shared/_server_variables.js.erb
+++ b/modules/backlogs/app/views/shared/_server_variables.js.erb
@@ -42,7 +42,7 @@ RB.i18n = {
};
RB.urlFor = (function () {
- var routes = {
+ const routes = {
update_sprint: '<%= backlogs_project_sprint_path(project_id: @project.identifier, id: ":id") %>',
create_story: '<%= backlogs_project_sprint_stories_path(project_id: @project.identifier, sprint_id: ":sprint_id") %>',
@@ -58,20 +58,20 @@ RB.urlFor = (function () {
};
return function (routeName, options) {
- route = routes[routeName];
+ let route = routes[routeName];
- if (options){
+ if (options) {
if (options.id) {
route = route.replace(":id", options.id);
}
- if (options.project_id){
+ if (options.project_id) {
route = route.replace(":project_id", options.project_id);
}
- if(options.sprint_id) {
- route = route.replace(":sprint_id", options.sprint_id)
+ if (options.sprint_id) {
+ route = route.replace(":sprint_id", options.sprint_id);
}
}
return route;
- }
+ };
}());
diff --git a/modules/backlogs/spec/support/pages/backlogs.rb b/modules/backlogs/spec/support/pages/backlogs.rb
index c6800118012a..7e8fe177624e 100644
--- a/modules/backlogs/spec/support/pages/backlogs.rb
+++ b/modules/backlogs/spec/support/pages/backlogs.rb
@@ -56,9 +56,9 @@ def alter_attributes_in_edit_story_mode(story, attributes)
field_name = WorkPackage.human_attribute_name(key)
case key
when :subject, :story_points
- fill_in field_name, with: value
+ fill_in field_name, with: value.to_s
when :status, :type
- select value, from: field_name
+ select value.to_s, from: field_name
else
raise NotImplementedError
end
@@ -211,7 +211,7 @@ def expect_for_story(story, attributes)
def expect_story_link_to_wp_page(story)
within_story(story) do
expect(page)
- .to have_link(story.id, href: work_package_path(story))
+ .to have_link(story.to_param, href: work_package_path(story))
end
end
diff --git a/modules/budgets/frontend/module/augment/planned-costs-form.ts b/modules/budgets/frontend/module/augment/planned-costs-form.ts
index f0286c6e208c..c26803f67d89 100644
--- a/modules/budgets/frontend/module/augment/planned-costs-form.ts
+++ b/modules/budgets/frontend/module/augment/planned-costs-form.ts
@@ -26,9 +26,11 @@
// See COPYRIGHT and LICENSE files for more details.
//++
+import { delegate } from '@knowledgecode/delegate';
+
export class PlannedCostsFormAugment {
static listen():void {
- jQuery(document).on('click', '.costs--edit-planned-costs-btn', (evt) => {
+ delegate(document).on('click', '.costs--edit-planned-costs-btn', (evt) => {
const link = evt.target as HTMLElement;
const form = link.nextElementSibling as HTMLElement;
@@ -39,7 +41,7 @@ export class PlannedCostsFormAugment {
input.disabled = false;
});
- jQuery(document).on('click', '.costs--edit-planned-costs-cancel-btn', (evt) => {
+ delegate(document).on('click', '.costs--edit-planned-costs-cancel-btn', (evt) => {
const form = (evt.target as HTMLElement).closest('.costs--edit-form') as HTMLElement;
const link = form.previousElementSibling as HTMLElement;
diff --git a/modules/costs/app/views/admin/cost_types/_list.html.erb b/modules/costs/app/views/admin/cost_types/_list.html.erb
index 4f2518df3f92..299d68a66999 100644
--- a/modules/costs/app/views/admin/cost_types/_list.html.erb
+++ b/modules/costs/app/views/admin/cost_types/_list.html.erb
@@ -128,20 +128,4 @@ See COPYRIGHT and LICENSE files for more details.
-<%# Moved from assets since this is the only remaining confirmation JS from the old costs code %>
-<%= nonced_javascript_tag do %>
- function submitForm(event, el) {
- submitFormWithConfirmation(event, el, true);
- }
-
- function submitFormWithConfirmation(event, el, withConfirmation) {
- event.preventDefault();
-
- if (!withConfirmation || confirm(I18n.t("js.text_are_you_sure"))) {
- jQuery(el).parent().submit();
- }
-
- return false;
- }
-<% end %>
<% end %>
diff --git a/modules/github_integration/frontend/module/git-actions-menu/git-actions-menu.directive.ts b/modules/github_integration/frontend/module/git-actions-menu/git-actions-menu.directive.ts
index 2fc26e0b892a..5e446816f142 100644
--- a/modules/github_integration/frontend/module/git-actions-menu/git-actions-menu.directive.ts
+++ b/modules/github_integration/frontend/module/git-actions-menu/git-actions-menu.directive.ts
@@ -45,7 +45,7 @@ export class GitActionsMenuDirective extends OpContextMenuTrigger {
super(elementRef, opContextMenu);
}
- protected open(evt:JQuery.TriggeredEvent) {
+ protected open(evt:Event) {
this.opContextMenu.show(this, evt, GitActionsMenuComponent);
}
diff --git a/modules/gitlab_integration/frontend/module/git-actions-menu/git-actions-menu.directive.ts b/modules/gitlab_integration/frontend/module/git-actions-menu/git-actions-menu.directive.ts
index c10dc493317b..b13a59037b35 100644
--- a/modules/gitlab_integration/frontend/module/git-actions-menu/git-actions-menu.directive.ts
+++ b/modules/gitlab_integration/frontend/module/git-actions-menu/git-actions-menu.directive.ts
@@ -47,7 +47,7 @@ export class GitActionsMenuDirective extends OpContextMenuTrigger {
super(elementRef, opContextMenu);
}
- protected open(evt:JQuery.TriggeredEvent) {
+ protected open(evt:Event) {
this.opContextMenu.show(this, evt, GitActionsMenuComponent);
}
diff --git a/modules/openid_connect/app/views/session/_warn_logout.js.erb b/modules/openid_connect/app/views/session/_warn_logout.js.erb
deleted file mode 100644
index 17fffe8f2480..000000000000
--- a/modules/openid_connect/app/views/session/_warn_logout.js.erb
+++ /dev/null
@@ -1,16 +0,0 @@
-// window.parent == OpenProject window
-var op = window.parent;
-var $ = op.jQuery
-var back_url = encodeURIComponent(op.document.location.href);
-
-$.ajax({
- url: "/session/logout_warning?back_url=" + back_url,
- cache: false,
- success: function(html) {
- $("#logout-warning").remove();
- $(html).prependTo("body").hide().slideDown();
- $('html, body').animate({
- scrollTop: $("#logout-warning").offset().top
- }, 1000);
- }
-});
diff --git a/spec/features/work_packages/export_spec.rb b/spec/features/work_packages/export_spec.rb
index 19c9a1b8978c..375332c56575 100644
--- a/spec/features/work_packages/export_spec.rb
+++ b/spec/features/work_packages/export_spec.rb
@@ -208,8 +208,7 @@ def expect_selected_columns(columns = %w[])
end
it "opens the dialog and exports" do
- settings_menu.open_and_choose I18n.t("js.toolbar.settings.export")
- click_on export_type
+ open_export_dialog!
export!
end
end
diff --git a/spec/helpers/removed_js_helpers_helper_spec.rb b/spec/helpers/removed_js_helpers_helper_spec.rb
index 910688fe0f9d..0df1f3be265b 100644
--- a/spec/helpers/removed_js_helpers_helper_spec.rb
+++ b/spec/helpers/removed_js_helpers_helper_spec.rb
@@ -31,24 +31,54 @@
require "spec_helper"
RSpec.describe RemovedJsHelpersHelper do
- include RemovedJsHelpersHelper
-
- describe "link_to_function" do
+ describe "#link_to_function" do
it "returns a valid link" do
allow(SecureRandom).to receive(:uuid).and_return "uuid"
- expect(self).to receive(:content_for).with(:additional_js_dom_ready)
- expect(link_to_function("blubs", nil))
+ expect(helper.link_to_function("blubs", nil))
.to be_html_eql %{
blubs
}
end
it "adds the provided method to the onclick handler" do
- expect(self).to receive(:content_for).with(:additional_js_dom_ready)
- expect(link_to_function("blubs", "doTheMagic(now)", id: :foo))
+ expect(helper.link_to_function("blubs", "doTheMagic(now)", id: :foo))
.to be_html_eql %{
blubs
}
end
end
+
+ describe "#csp_onclick" do
+ it "generates a 'click' event handler for the element" do
+ helper.csp_onclick("console.log('hello');", "#my-element")
+
+ expect(helper.content_for(:additional_js_dom_ready)).to eq(<<~JS)
+ document.querySelector('#my-element')?.addEventListener('click', function(event) {
+ console.log('hello');
+ event.preventDefault();
+ });
+ JS
+ end
+
+ it "generates a 'click' event handler for the element that does not call event.preventDefault()" do
+ helper.csp_onclick("console.log('hello');", "#my-element", prevent_default: false)
+
+ expect(helper.content_for(:additional_js_dom_ready)).to eq(<<~JS)
+ document.querySelector('#my-element')?.addEventListener('click', function(event) {
+ console.log('hello');
+ });
+ JS
+ end
+
+ it "escapes selector" do
+ helper.csp_onclick("console.log('hello');", "[data-attr^='foo']")
+
+ expect(helper.content_for(:additional_js_dom_ready)).to eq(<<~JS)
+ document.querySelector('[data-attr^=\\'foo\\']')?.addEventListener('click', function(event) {
+ console.log('hello');
+ event.preventDefault();
+ });
+ JS
+ end
+ end
end
diff --git a/spec/support/components/attachments/attachments.rb b/spec/support/components/attachments/attachments.rb
index bdde4991c08f..7e2c7f7c8cdb 100644
--- a/spec/support/components/attachments/attachments.rb
+++ b/spec/support/components/attachments/attachments.rb
@@ -18,7 +18,7 @@ def drag_and_drop_file(target,
scroll: true)
# Remove any previous input, if any
page.execute_script <<-JS
- jQuery('#temporary_attachment_files').remove()
+ document.getElementById('temporary_attachment_files')?.remove()
JS
if stopover.is_a?(Array) && !stopover.all?(String)
diff --git a/spec/support/components/attachments/attachments_input.js b/spec/support/components/attachments/attachments_input.js
index cd42573992b9..77a10ec19ef0 100644
--- a/spec/support/components/attachments/attachments_input.js
+++ b/spec/support/components/attachments/attachments_input.js
@@ -103,43 +103,44 @@ function dropOnTarget(dataTransfer) {
});
}
-let input = jQuery(' ')
- .attr('id', name)
- .attr('name', name)
- .attr('type', 'file')
- .attr('style', 'position:fixed;left:0;bottom:0;z-index:10000')
- .appendTo(document.body)
- .on('change', function(event) {
- input.remove();
- event.stopPropagation();
-
- let dataTransfer = {
- constructor : DataTransfer,
- effectAllowed : 'all',
- dropEffect : 'none',
- types : [ 'Files' ],
- files : input[0].files,
- setData : function setData(){},
- getData : function getData(){},
- clearData : function clearData(){},
- setDragImage : function setDragImage(){}
- };
-
- // If we have stopovers, do those first and then get the target
- if (stopovers.length > 0) {
- stopovers.forEach((stopover) => dropOnStopover(stopover, dataTransfer));
-
- setTimeout(() => {
- if (!cancelDrop) {
- // After we left the stopover DOM elements, the target element should remain visible.
- // If it's not visible, we raise an error.
- if (target.offsetParent === null) {
- throw new Error("Cannot drop the file on an invisible target");
- };
- dropOnTarget(dataTransfer);
- }
- }, 2000);
- } else {
+let input = document.createElement('input');
+input.id = name;
+input.name = name;
+input.type = 'file';
+input.style.cssText = 'position:fixed;left:0;bottom:0;z-index:10000';
+document.body.appendChild(input);
+
+input.addEventListener('change', function(event) {
+ input.remove();
+ event.stopPropagation();
+
+ let dataTransfer = {
+ constructor : DataTransfer,
+ effectAllowed : 'all',
+ dropEffect : 'none',
+ types : [ 'Files' ],
+ files : input.files,
+ setData : function setData(){},
+ getData : function getData(){},
+ clearData : function clearData(){},
+ setDragImage : function setDragImage(){}
+ };
+
+ // If we have stopovers, do those first and then get the target
+ if (stopovers.length > 0) {
+ stopovers.forEach((stopover) => dropOnStopover(stopover, dataTransfer));
+
+ setTimeout(() => {
+ if (!cancelDrop) {
+ // After we left the stopover DOM elements, the target element should remain visible.
+ // If it's not visible, we raise an error.
+ if (target.offsetParent === null) {
+ throw new Error("Cannot drop the file on an invisible target");
+ };
dropOnTarget(dataTransfer);
}
- });
+ }, 2000);
+ } else {
+ dropOnTarget(dataTransfer);
+ }
+});
diff --git a/spec/support/components/wysiwyg/wysiwyg_editor.rb b/spec/support/components/wysiwyg/wysiwyg_editor.rb
index e9ba8e93d93a..4e46c1113181 100644
--- a/spec/support/components/wysiwyg/wysiwyg_editor.rb
+++ b/spec/support/components/wysiwyg/wysiwyg_editor.rb
@@ -35,7 +35,7 @@ def set_markdown(text)
textarea = container.find(".op-ckeditor-source-element", visible: :all)
page.execute_script(
- 'jQuery(arguments[0]).trigger("op:ckeditor:setData", arguments[1])',
+ 'arguments[0].dispatchEvent(new CustomEvent("op:ckeditor:setData", { detail: arguments[1] }))',
textarea.native,
text
)
@@ -44,7 +44,7 @@ def set_markdown(text)
def clear
textarea = container.find(".op-ckeditor-source-element", visible: :all)
page.execute_script(
- 'jQuery(arguments[0]).trigger("op:ckeditor:clear")',
+ 'arguments[0].dispatchEvent(new Event("op:ckeditor:clear"))',
textarea.native
)
end
@@ -52,7 +52,7 @@ def clear
def trigger_autosave
textarea = container.find(".op-ckeditor-source-element", visible: :all)
page.execute_script(
- 'jQuery(arguments[0]).trigger("op:ckeditor:autosave")',
+ 'arguments[0].dispatchEvent(new Event("op:ckeditor:autosave"))',
textarea.native
)
end
diff --git a/spec/support/shared/loading_indicator_saveguard.rb b/spec/support/shared/loading_indicator_saveguard.rb
index 8b43f640dde1..e9ee67841d6f 100644
--- a/spec/support/shared/loading_indicator_saveguard.rb
+++ b/spec/support/shared/loading_indicator_saveguard.rb
@@ -28,7 +28,7 @@
# See COPYRIGHT and LICENSE files for more details.
#++
-# Method to manually wait for an asynchronous request (through jQuery) to complete.
+# Method to manually wait for an asynchronous request to complete.
# This applies to all requests through resources as well.
#
# Note: Use this only if there are no other means of detecting the successful