diff --git a/configure/package.json b/configure/package.json index cffa7424d..3ed42e47c 100644 --- a/configure/package.json +++ b/configure/package.json @@ -1,6 +1,6 @@ { "name": "configure", - "version": "5.2.5-20260622", + "version": "5.2.7-20260629", "homepage": "./configure/build", "private": true, "dependencies": { diff --git a/package.json b/package.json index 16a6b67ce..e07342ecb 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "mmgis", - "version": "5.2.5-20260622", + "version": "5.2.7-20260629", "description": "A web-based mapping and localization solution for science operation on planetary missions.", "homepage": "build", "repository": { diff --git a/src/essence/Basics/Layers_/Filtering/Filtering.css b/src/essence/Basics/Layers_/Filtering/Filtering.css index a8ae078bd..c5e352cd6 100644 --- a/src/essence/Basics/Layers_/Filtering/Filtering.css +++ b/src/essence/Basics/Layers_/Filtering/Filtering.css @@ -183,6 +183,7 @@ } .layersTool_filtering_value_operator { box-sizing: border-box; + width: 30px; } .layersTool_filtering_value .layersTool_filtering_value_value { flex: 1; @@ -264,14 +265,6 @@ .layersTool_filtering_group_operator_select.op_not_or { background: var(--color-p4); } -.layersTool_filtering_group_operator_select .dropy__title span { - padding: 4px 0px !important; - width: 100%; - font-size: 12px; - font-weight: bold; - letter-spacing: 1px; - text-align: center !important; -} .layersTool_filtering_value_operator_select:hover { background: var(--color-a2); @@ -301,26 +294,6 @@ background: #fffb80; } -#layersTool_filtering .dropy, -#layersTool_filtering .dropy__title { - height: 30px; - border: none; -} -#layersTool_filtering .dropy__title span { - padding: 6px; -} -#layersTool_filtering .dropy .dropy__content li a { - padding: 6px; - border-top: 1px solid var(--color-m); - margin-bottom: 0px; - height: 30px; -} -#layersTool_filtering .dropy__content .dropy__header { - display: none; -} -#layersTool_filtering .dropy__content ul { - transition: none; -} #layersTool_filtering_submit_loading { display: none; diff --git a/src/essence/Basics/Layers_/Filtering/Filtering.js b/src/essence/Basics/Layers_/Filtering/Filtering.js index 8de1f15d0..eae549bbd 100644 --- a/src/essence/Basics/Layers_/Filtering/Filtering.js +++ b/src/essence/Basics/Layers_/Filtering/Filtering.js @@ -10,7 +10,7 @@ import ESFilterer from './ESFilterer' import GeodatasetFilterer from './GeodatasetFilterer' import Help from '../../UserInterface_/components/Help/Help' -import Dropy from '../../../../external/Dropy/dropy' +import OpGridSelector from './OpGridSelector' import { circle } from '@turf/turf' import Sortable from 'sortablejs' @@ -191,6 +191,7 @@ const Filtering = { destroy: function () { // Clear Spatial Filter Map_.rmNotNull(Filtering.mapSpatialLayer) + OpGridSelector.destroy() $('#layersTool_filtering').remove() }, @@ -520,58 +521,31 @@ const Filtering = { Filtering.setSubmitButtonState(true) }) - // Operator Dropdown + // Operator Grid Selector elmId = `#layersTool_filtering_group_operator_${F_.getSafeName( layerName )}_${id}` const ops = ['AND', 'OR', 'NOT_AND', 'NOT_OR'] const opId = Math.max(ops.indexOf(options.op), 0) - $(elmId).html( - Dropy.construct( - [ - `
All Must Match (AND)
`, - `
Any May Match (OR)
`, - `
Not All May Match (NOT AND)
`, - `
None Must Match (NOT OR)
`, - ], - 'op', - opId, - { openUp: true, hideChevron: true } - ) - ) - Dropy.init($(elmId), function (idx) { - const newOp = ops[idx] - Filtering.filters[layerName].values[id].op = newOp - switch (newOp) { - case 'AND': - $(elmId).removeClass('op_or') - $(elmId).removeClass('op_not_and') - $(elmId).removeClass('op_not_or') - $(elmId).addClass('op_and') - break - case 'OR': - $(elmId).removeClass('op_and') - $(elmId).removeClass('op_not_and') - $(elmId).removeClass('op_not_or') - $(elmId).addClass('op_or') - break - case 'NOT_AND': - $(elmId).removeClass('op_and') - $(elmId).removeClass('op_or') - $(elmId).removeClass('op_not_or') - $(elmId).addClass('op_not_and') - break - case 'NOT_OR': - $(elmId).removeClass('op_and') - $(elmId).removeClass('op_or') - $(elmId).removeClass('op_not_and') - $(elmId).addClass('op_not_or') - break - default: - break - } - Filtering.setSubmitButtonState(true) + + const groupOpItems = [ + { html: `
AND
`, title: 'All Must Match (AND)' }, + { html: `
OR
`, title: 'Any May Match (OR)' }, + { html: `
NAND
`, title: 'Not All May Match (NOT AND)' }, + { html: `
NOR
`, title: 'None Must Match (NOT OR)' }, + ] + + OpGridSelector.init($(elmId), groupOpItems, opId, { + columns: 4, + onSelect: function (idx) { + const newOp = ops[idx] + Filtering.filters[layerName].values[id].op = newOp + $(elmId) + .removeClass('op_and op_or op_not_and op_not_or') + .addClass('op_' + newOp.toLowerCase()) + Filtering.setSubmitButtonState(true) + }, }) }, attachValueEvents: function (id, layerName, options) { @@ -696,7 +670,7 @@ const Filtering = { } else $(this).css('border', '1px solid var(--color-p4)') }) - // Operator Dropdown + // Operator Grid Selector elmId = `#layersTool_filtering_value_operator_${F_.getSafeName( layerName )}_${id}` @@ -714,28 +688,26 @@ const Filtering = { 'endswith', ] const opId = Math.max(ops.indexOf(options.op), 0) - $(elmId).html( - Dropy.construct( - [ - ``, - `
!=
`, - `
in
`, - ``, - ``, - ``, - ``, - ``, - ``, - ``, - ], - 'op', - opId, - { openUp: true, hideChevron: true } - ) - ) - Dropy.init($(elmId), function (idx) { - Filtering.filters[layerName].values[id].op = ops[idx] - Filtering.setSubmitButtonState(true) + + const valueOpItems = [ + { html: ``, title: 'Equals' }, + { html: `
!=
`, title: 'Not Equals' }, + { html: `
in
`, title: 'Comma-separated list' }, + { html: ``, title: 'Less than' }, + { html: ``, title: 'Greater than' }, + { html: ``, title: 'Less than or Equal' }, + { html: ``, title: 'Greater than or Equal' }, + { html: ``, title: 'Contains' }, + { html: ``, title: 'Begins With' }, + { html: ``, title: 'Ends With' }, + ] + + OpGridSelector.init($(elmId), valueOpItems, opId, { + columns: 5, + onSelect: function (idx) { + Filtering.filters[layerName].values[id].op = ops[idx] + Filtering.setSubmitButtonState(true) + }, }) // Value AutoComplete diff --git a/src/essence/Basics/Layers_/Filtering/OpGridSelector.css b/src/essence/Basics/Layers_/Filtering/OpGridSelector.css new file mode 100644 index 000000000..93b821001 --- /dev/null +++ b/src/essence/Basics/Layers_/Filtering/OpGridSelector.css @@ -0,0 +1,45 @@ +.op-grid-trigger { + display: flex; + align-items: center; + justify-content: center; + width: 100%; + height: 100%; + cursor: pointer; + transition: background 0.2s cubic-bezier(0.785, 0.135, 0.15, 0.86); +} +.op-grid-trigger:hover { + background: var(--color-a3); +} + +.op-grid-popup { + display: grid; + background: var(--color-a1); + border: 1px solid var(--color-a4); + border-radius: 3px; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.4); + padding: 2px; + gap: 1px; +} + +.op-grid-item { + display: flex; + align-items: center; + justify-content: center; + height: 28px; + min-width: 28px; + padding: 2px 6px; + cursor: pointer; + border-radius: 2px; + color: var(--color-f); + transition: background 0.1s ease; +} +.op-grid-item:hover { + background: var(--color-a4); +} +.op-grid-item--selected { + background: var(--color-c); + font-weight: bold; +} +.op-grid-item--selected:hover { + background: var(--color-c2); +} diff --git a/src/essence/Basics/Layers_/Filtering/OpGridSelector.js b/src/essence/Basics/Layers_/Filtering/OpGridSelector.js new file mode 100644 index 000000000..73ebd3660 --- /dev/null +++ b/src/essence/Basics/Layers_/Filtering/OpGridSelector.js @@ -0,0 +1,131 @@ +// Compact grid-based operator selector for the Filtering tool. +// Renders a trigger button showing the current selection and a +// fixed-position popup grid that escapes ancestor overflow clipping. + +import $ from 'jquery' + +import './OpGridSelector.css' + +let activePopup = null +let scrollHandler = null +let clickHandler = null + +function closeActivePopup() { + if (activePopup) { + activePopup.remove() + activePopup = null + } + if (scrollHandler) { + document.removeEventListener('scroll', scrollHandler, true) + scrollHandler = null + } + if (clickHandler) { + document.removeEventListener('mousedown', clickHandler, true) + clickHandler = null + } +} + +/** + * Initialize an operator grid selector. + * @param {jQuery} $container - The container element to render into + * @param {Array} items - Array of { html, title } for each grid cell + * @param {number} selectedIdx - Initially selected index + * @param {object} opts - { columns, onSelect } + * columns: number of grid columns (default 4) + * onSelect(idx): callback fired when an item is selected + */ +function init($container, items, selectedIdx, opts = {}) { + const columns = opts.columns || 4 + const onSelect = opts.onSelect || (() => {}) + + let currentIdx = selectedIdx + + // Render trigger button showing current selection + const triggerHtml = `
${items[currentIdx].html}
` + $container.html(triggerHtml) + + const $trigger = $container.find('.op-grid-trigger') + + $trigger.on('click', function (e) { + e.stopPropagation() + + // If this popup is already open, close it + if (activePopup && activePopup.data('owner') === $container[0]) { + closeActivePopup() + return + } + + // Close any other open popup + closeActivePopup() + + // Build grid popup + let gridHtml = `
` + for (let i = 0; i < items.length; i++) { + const selectedClass = i === currentIdx ? ' op-grid-item--selected' : '' + gridHtml += `
${items[i].html}
` + } + gridHtml += '
' + + const $popup = $(gridHtml) + $popup.data('owner', $container[0]) + $('body').append($popup) + activePopup = $popup + + // Position: fixed relative to trigger + const bcr = $trigger[0].getBoundingClientRect() + const openDown = bcr.top < window.innerHeight / 2 + + $popup.css({ + position: 'fixed', + left: bcr.left, + zIndex: 10000, + }) + + if (openDown) { + $popup.css('top', bcr.bottom + 2) + } else { + // Measure popup height then position above trigger + $popup.css({ top: 0, visibility: 'hidden' }) + const popupHeight = $popup.outerHeight() + $popup.css({ + top: bcr.top - popupHeight - 2, + visibility: 'visible', + }) + } + + // Item click + $popup.on('click', '.op-grid-item', function () { + const idx = parseInt($(this).attr('data-idx'), 10) + currentIdx = idx + $trigger.html(items[idx].html) + onSelect(idx) + closeActivePopup() + }) + + // Close on scroll (capture phase catches nested scrollables) + scrollHandler = () => closeActivePopup() + document.addEventListener('scroll', scrollHandler, true) + + // Close on outside click + clickHandler = (evt) => { + if ( + activePopup && + !activePopup[0].contains(evt.target) && + !$trigger[0].contains(evt.target) + ) { + closeActivePopup() + } + } + // Delay to avoid catching the current click + setTimeout(() => { + document.addEventListener('mousedown', clickHandler, true) + }, 0) + }) +} + +// Clean up any open popup (call on destroy) +function destroy() { + closeActivePopup() +} + +export default { init, destroy }