diff --git public/javascripts/context_menu.js public/javascripts/context_menu.js index 2c5886364..95ee4d365 100644 --- public/javascripts/context_menu.js +++ public/javascripts/context_menu.js @@ -1,14 +1,16 @@ /* Redmine - project management software Copyright (C) 2006-2022 Jean-Philippe Lang */ -var observing; +let observing; function rightClick(event) { - var target = $(event.target); + const target = $(event.target); if (target.is('a:not(.js-contextmenu)')) {return;} - var tr = target.closest('.hascontextmenu').first(); + + const tr = target.closest('.hascontextmenu').first(); if (tr.length < 1) {return;} event.preventDefault(); + if (!isSelected(tr)) { unselectAll(); addSelection(tr); @@ -18,8 +20,7 @@ function rightClick(event) { } function click(event) { - var target = $(event.target); - var lastSelected; + const target = $(event.target); if (target.is('a') && target.hasClass('submenu')) { event.preventDefault(); @@ -27,48 +28,12 @@ function click(event) { } hide(); if (target.is('a') || target.is('img')) { return; } + if (event.which == 1 || (navigator.appVersion.match(/\bMSIE\b/))) { - var tr = target.closest('.hascontextmenu').first(); + const tr = target.closest('.hascontextmenu').first(); if (tr.length > 0) { // a row was clicked - if (target.is('td.checkbox')) { - // the td containing the checkbox was clicked, toggle the checkbox - target = target.find('input').first(); - target.prop("checked", !target.prop("checked")); - } - if (target.is('input')) { - // a checkbox may be clicked - if (target.prop('checked')) { - tr.addClass('context-menu-selection'); - } else { - tr.removeClass('context-menu-selection'); - } - } else { - if (event.ctrlKey || event.metaKey) { - toggleSelection(tr); - clearDocumentSelection(); - } else if (event.shiftKey) { - lastSelected = getLastSelected(); - if (lastSelected.length) { - var toggling = false; - $('.hascontextmenu').each(function(){ - if (toggling || $(this).is(tr)) { - addSelection($(this)); - clearDocumentSelection(); - } - if ($(this).is(tr) || $(this).is(lastSelected)) { - toggling = !toggling; - } - }); - } else { - addSelection(tr); - } - } else { - unselectAll(); - addSelection(tr); - } - setLastSelected(tr); - } + selectRows(target, tr, event); } else { // click is outside the rows if (target.is('a') && (target.hasClass('disabled') || target.hasClass('submenu'))) { @@ -82,85 +47,141 @@ function click(event) { } } +export function selectRows($element, row, { ctrlKey, metaKey, shiftKey } ) { + let $target = $element; + if ($target.is('td.checkbox')) { + // the td containing the checkbox was clicked, toggle the checkbox + $target = $target.find('input').first(); + $target.prop("checked", !$target.prop("checked")); + } + if ($target.is('input')) { + // a checkbox may be clicked + row.toggleClass('context-menu-selection', $target.prop('checked')) + } else { + if (ctrlKey || metaKey) { + toggleSelection(row); + clearDocumentSelection(); + } else if (shiftKey) { + const lastSelected = getLastSelected(); + if (lastSelected.length) { + const selected = addMultipleSelection($('.hascontextmenu'), lastSelected, row); + selected.forEach($e => addSelection($e)); + } else { + addSelection(row); + } + } else { + unselectAll(); + addSelection(row); + } + setLastSelected(row); + } +} + +export function addMultipleSelection($rows, lastSelected, clicked) { + let toggling = false; + const selected = []; + $rows.each((i, elm) => { + const $elm = $(elm); + if (!$elm.is(lastSelected) && (toggling || $elm.is(clicked))) { + selected.push($elm); + clearDocumentSelection(); + } + if ($elm.is(lastSelected) !== $elm.is(clicked)) { + toggling = !toggling; + } + }); + return selected; +} + function create() { if ($('#context-menu').length < 1) { - var menu = document.createElement("div"); + const menu = document.createElement("div"); menu.setAttribute("id", "context-menu"); menu.setAttribute("style", "display:none;"); document.getElementById("content").appendChild(menu); } } -function show(event) { - var mouse_x = event.pageX; - var mouse_y = event.pageY; - var mouse_y_c = event.clientY; - var render_x = mouse_x; - var render_y = mouse_y; - var dims; - var menu_width; - var menu_height; - var window_width; - var window_height; - var max_width; - var max_height; - var url; - - $('#context-menu').css('left', (render_x + 'px')); - $('#context-menu').css('top', (render_y + 'px')); - $('#context-menu').html(''); - - url = $(event.target).parents('form').first().data('cm-url'); +function show({ pageX: mouse_x, pageY: mouse_y, clientY: mouse_y_c, target: target }) { + const $element = $('#context-menu'); + $element.css({ left: `${mouse_x}px`, top: `${mouse_y}px` }); + $element.html(''); + + const form = $(target).parents('form').first(); + const url = form.data('cm-url'); if (url == null) {alert('no url'); return;} $.ajax({ url: url, - data: $(event.target).parents('form').first().serialize(), - success: function(data, textStatus, jqXHR) { - $('#context-menu').html(data); - menu_width = $('#context-menu').width(); - menu_height = $('#context-menu').height(); - max_width = mouse_x + 2*menu_width; - max_height = mouse_y_c + menu_height; - - var ws = window_size(); - window_width = ws.width; - window_height = ws.height; - - /* display the menu above and/or to the left of the click if needed */ - if (max_width > window_width) { - render_x -= menu_width; - $('#context-menu').addClass('reverse-x'); - } else { - $('#context-menu').removeClass('reverse-x'); - } + data: form.serialize(), + success: (data, textStatus, jqXHR) => { + $element.html(data); - if (max_height > window_height) { - render_y -= menu_height; - $('#context-menu').addClass('reverse-y'); - // adding class for submenu - if (mouse_y_c < 325) { - $('#context-menu .folder').addClass('down'); - } - } else { - // adding class for submenu - if (window_height - mouse_y_c < 345) { - $('#context-menu .folder').addClass('up'); - } - $('#context-menu').removeClass('reverse-y'); - } + const menu_width = $element.width(); + const menu_height = $element.height(); + const max_width = mouse_x + 2 * menu_width; + const max_height = mouse_y_c + menu_height; + const ws = window_size(); + + const is_reverse_x = max_width > ws.width; + const arg_x = { render_pos: mouse_x, menu_size: menu_width, position: 'left', class_name: 'reverse_x' } + const action_x = is_reverse_x ? reverseRenderAction(arg_x) + : normalRenderAction(arg_x); - if (render_x <= 0) render_x = 1; - if (render_y <= 0) render_y = 1; - $('#context-menu').css('left', (render_x + 'px')); - $('#context-menu').css('top', (render_y + 'px')); - $('#context-menu').show(); + const is_reverse_y = max_height > ws.height; + const arg_y = { render_pos: mouse_y, menu_size: menu_height, position: 'top', class_name: 'reverse_y' } + const action_y = is_reverse_y ? reverseRenderAction(arg_y) + : normalRenderAction(arg_y); - //if (window.parseStylesheets) { window.parseStylesheets(); } // IE + const arg_submenu = { window_height: ws.height, mouse_y_c } + const action_submenu = is_reverse_y ? reverseFolderAction(arg_submenu) + : normalFolderAction(arg_submenu); + action_x($element); + action_y($element); + // adding class for submenu + action_submenu($element); + + $element.show(); } }); } +export function reverseRenderAction({ render_pos, menu_size, position, class_name }) { + return element => { + element.addClass(class_name) + const n = adjustLessThanZero(render_pos - menu_size); + element.css(position, `${n}px`); + } +} + +export function normalRenderAction({ render_pos, menu_size, position, class_name }) { + return element => { + element.removeClass(class_name); + const n = adjustLessThanZero(render_pos); + element.css(position, `${n}px`); + } +} + +export function reverseFolderAction({ window_height, mouse_y_c }) { + return element => { + if (mouse_y_c < 325) { + element.find('.folder').addClass('down'); + } + } +} + +export function normalFolderAction({ window_height, mouse_y_c }) { + return element => { + if (window_height - mouse_y_c < 345) { + element.find('.folder').addClass('up'); + } + } +} + +function adjustLessThanZero(n) { + return n <= 0 ? 1 : n; +} + function setLastSelected(tr) { $('.cm-last').removeClass('cm-last'); tr.addClass('cm-last'); @@ -172,8 +193,8 @@ function getLastSelected() { function unselectAll() { $('input[type=checkbox].toggle-selection').prop('checked', false); - $('.hascontextmenu').each(function(){ - removeSelection($(this)); + $('.hascontextmenu').each((i, el) => { + removeSelection($(el)); }); $('.cm-last').removeClass('cm-last'); } @@ -220,24 +241,26 @@ function clearDocumentSelection() { function init() { create(); unselectAll(); - + if (!observing) { $(document).click(click); $(document).contextmenu(rightClick); $(document).on('click', '.js-contextmenu', rightClick); observing = true; } + $('input[type=checkbox].toggle-selection').on('change', toggleIssuesSelection); } -function toggleIssuesSelection(el) { - var checked = $(this).prop('checked'); - var boxes = $(this).parents('table').find('input[name=ids\\[\\]]'); +function toggleIssuesSelection(event) { + const $target = $(event.target) + const checked = $target.prop('checked'); + const boxes = $target.parents('table').find('input[name=ids\\[\\]]'); boxes.prop('checked', checked).parents('.hascontextmenu').toggleClass('context-menu-selection', checked); } function window_size() { - var w; - var h; + let w; + let h; if (window.innerWidth) { w = window.innerWidth; h = window.innerHeight; @@ -251,7 +274,4 @@ function window_size() { return {width: w, height: h}; } -$(document).ready(function(){ - init(); - $('input[type=checkbox].toggle-selection').on('change', toggleIssuesSelection); -}); +init(); diff --git test/javascripts/context_menu.html test/javascripts/context_menu.html new file mode 100644 index 000000000..d9ba3e619 --- /dev/null +++ test/javascripts/context_menu.html @@ -0,0 +1,23 @@ + +
+ + + +