Skip to content

add back count display. allow selected items to be sorted to top - #137

Open
jaredgibb wants to merge 25 commits into
weweb-assets:mainfrom
jaredgibb:main
Open

add back count display. allow selected items to be sorted to top#137
jaredgibb wants to merge 25 commits into
weweb-assets:mainfrom
jaredgibb:main

Conversation

@jaredgibb

Copy link
Copy Markdown

No description provided.

Summary
I've successfully implemented the "Sort Selected to Top" feature for your WeWeb select component. Here's what was added:

1. Configuration (ww-config.js)
New Property: sortSelectedToTop
Type: Toggle (On/Off)
Default: false (off)
Location: Settings → Options section
Supports binding, states, and responsive settings
2. Component Logic (wwElement_OptionsList.vue)
Injected Dependencies: Added access to selectedValue and mappingValue from the parent component
Enhanced filteredOptions Computed Property:
First applies search filtering (if active)
Then applies sorting logic when sortSelectedToTop is enabled
Compares each option's value against the selected value(s)
Handles both primitive values and objects
Handles both single and multiple selection modes
Uses JSON comparison for complex object values with fallback to direct comparison
How It Works:
When disabled (default): Options display in their original order
When enabled:
Selected items automatically move to the top of the list
Maintains relative order within selected and unselected groups
Works seamlessly with search filtering
Updates dynamically as items are selected/deselected
The feature integrates perfectly with:

Single and multiple select modes
Search functionality
Virtual scrolling (both heavy and dynamic modes)
All option types (text, icon+text, image+text)
Primitive and object-based options
Users can now enable this in the WeWeb editor under Settings → Options → Sort selected to top!
Corrected the closing comment for wwEditor to include a period, ensuring proper syntax and consistency.
Introduced multiple console.log statements throughout the OptionsList component to aid in debugging selection sorting, option filtering, and dynamic scroller item generation. Also added error handling for the sorting logic to improve traceability of issues during runtime.
Changed the watcher to track search state and filtered count instead of filteredOptions, ensuring updates only occur on search changes and not sorting. This improves accuracy when updating search results.
Eliminated multiple console.log statements from wwElement_OptionsList.vue to clean up debug output. Added targeted logging in wwElement_Select.vue for toggleValueAccessibility and closeDropdown functions to aid in debugging specific behaviors.
Introduced a watcher on the isOpen state to log transitions and stack traces for debugging purposes in the Select component.
Added Vue watchers to log changes to props.content reference and initialState for debugging purposes. These logs help track state changes and reference updates in the Select component.
Introduces an isSorting reactive flag to coordinate sorting state between Select and OptionsList components. Prevents dropdown from closing during sorting and ensures UI updates are handled smoothly.
Introduced console.log statements in wwElement_Option.vue and wwElement_Select.vue to provide detailed runtime information for option selection, registration, and unregistration events. This will help with debugging user interactions and option management.
Inserted multiple console.log statements throughout the options filtering, sorting, and search logic in wwElement_OptionsList.vue to aid in debugging and tracking state changes during computation and watcher triggers.
@jaredgibb jaredgibb closed this Nov 18, 2025
@jaredgibb jaredgibb reopened this Nov 18, 2025
@jaredgibb

Copy link
Copy Markdown
Author

greate

Inserted console.log statements in debouncedUpdateSearch and handleInputChange to aid debugging and trace search input events and updates.
Removed unused searchFilteredCount and watcher from OptionsList to prevent infinite loops. Search filtering and searchMatches computation are now handled directly in the Search component, improving separation of concerns and reliability.
Eliminated various console.log calls used for debugging in wwElement_Option.vue, wwElement_OptionsList.vue, wwElement_Search.vue, and wwElement_Select.vue to clean up the codebase and improve performance.
@jaredgibb jaredgibb changed the title push add back count display. allow selected items to be sorted to top Nov 18, 2025
Add a new 'changeOneItem' trigger/event and wire it through the select components. AI.md and ww-config.js document/register the new event. wwElement_Select.vue: refactor updateValue to accept an optional oneItemValue, centralize event emission into emitChangeEvents (which emits both 'change' and optionally 'changeOneItem'), and adjust multi/single-select handling to pass the per-item value when appropriate. wwElement_Option.vue: pass the option value when unselecting so the per-item event can be emitted. This enables listening for single-item changes without parsing the full value payload.
Copilot AI review requested due to automatic review settings February 23, 2026 15:25

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR adds two main features to the input select component: a count display mode for multi-select that shows the number of selected items instead of individual chips, and the ability to sort selected items to the top of the options list.

Changes:

  • Added display mode configuration (chips/count) for multi-select with customizable count text template
  • Implemented sorting functionality to move selected options to the top of the list
  • Added new changeOneItem event triggered when a single item is selected/unselected
  • Modified unselect behavior to automatically enable when sortSelectedToTop is active

Reviewed changes

Copilot reviewed 7 out of 7 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
ww-config.js Added configuration properties for displayMode, displayText, sortSelectedToTop, and changeOneItem event
src/wwElement_Trigger.vue Implemented conditional rendering for chips vs count display mode with count text templating
src/wwElement_Select.vue Added emitChangeEvents helper, isSorting flag for click-outside prevention, and oneItemValue parameter for fine-grained event emission
src/wwElement_Search.vue Added filterOptions function and searchMatches computation for local context exposure
src/wwElement_OptionsList.vue Implemented sorting logic for selected items, changed ID generation to use content-based stable IDs
src/wwElement_Option.vue Modified unselect behavior to automatically enable when sortSelectedToTop is active
AI.md Documented the new changeOneItem event
Comments suppressed due to low confidence (10)

src/wwElement_OptionsList.vue:213

  • Using setTimeout to reset isSorting after a fixed delay is fragile and may cause race conditions. If the dropdown closes before the timeout fires, the flag will be reset after the component is no longer mounted, or if sorting happens again within 100ms, the flag might be reset prematurely. Consider resetting the flag synchronously after the sort completes, or use a more deterministic approach like watching for when the computed property finishes evaluating.
                    setTimeout(() => {
                        isSorting.value = false;
                    }, 100);

src/wwElement_OptionsList.vue:242

  • The sort operation creates stable IDs based on JSON.stringify(item) which can be expensive for large objects or arrays. Additionally, using JSON.stringify for object identity is problematic because object property order may vary, circular references will throw errors (caught but resulting in fallback), and it doesn't handle functions, undefined, or symbols properly. Consider using a WeakMap or Symbol to create truly unique identifiers for objects.
                    const stableId = `primitive_${JSON.stringify(item)}`;
                    return { value: item, id: stableId };
                } else {
                    // For objects, use existing id or create stable ID based on the object's value
                    const existingId = item.id;
                    if (existingId != null) {
                        return { ...item, id: existingId };
                    }
                    // Create stable ID based on object content to survive re-sorting
                    try {
                        const stableId = `obj_${JSON.stringify(item)}`;
                        return { ...item, id: stableId };

src/wwElement_Trigger.vue:336

  • The template.replace method only replaces the first occurrence of '{count}'. If the template string contains multiple instances of '{count}', only the first will be replaced. Use template.replaceAll('{count}', count.toString()) or a global regex replace to handle all occurrences correctly.
            return template.replace('{count}', count.toString());

src/wwElement_Option.vue:285

  • Automatically enabling unselectOnClick when sortSelectedToTop is enabled changes user-expected behavior and could be surprising. This implicit coupling means that enabling sorting will also enable unselecting, which may not be desired. Users might want sorted items at the top but still require unselectOnClick to be explicitly false. Consider making this behavior explicit in the documentation or providing a separate configuration option, rather than having one feature implicitly enable another.
            const unselectOnClick = props.content.unselectOnClick ?? false;
            const selectOnClick = props.content.selectOnClick ?? true;
            const sortSelectedToTop = props.content.sortSelectedToTop ?? false;
            
            // Allow unselecting if either:
            // 1. unselectOnClick is explicitly enabled, OR
            // 2. sortSelectedToTop is enabled (natural UX for sorted items)
            const canUnselect = unselectOnClick || sortSelectedToTop;

src/wwElement_OptionsList.vue:173

  • The condition checks 'selectedValue.value != null', which will be true even when selectedValue is an empty array [] in multi-select mode. This means sorting logic will run unnecessarily when no items are selected, performing expensive operations for no benefit. Consider checking for 'selectedValue.value?.length > 0' for arrays or a more specific null/undefined/empty check.
            if (props.content.sortSelectedToTop && selectedValue.value != null) {

src/wwElement_Search.vue:101

  • The searchMatches computed in the Search component appears to be redundant. The OptionsList component performs its own filtering using memoizedFilter and doesn't use searchMatches for filtering - it only exposes it in the local context for external consumption. Computing searchMatches here duplicates the filtering work that's already happening in OptionsList, adding unnecessary performance overhead. If searchMatches is only needed for the local context, consider computing it in Select.vue where the local context is assembled, or remove it if it's not actually being used.
                // Compute searchMatches here in the Search component
                const searchMatches = value ? filterOptions(options.value, value) : [];
                updateSearch({ value, searchBy, searchMatches });

ww-config.js:1097

  • There's a typo in the comment closing tag. It should be "wwEditor:end" not "wwEditor:end ." (with a dot and space before the asterisk).
            /* wwEditor:end .*/

src/wwElement_Search.vue:95

  • The filterOptions function in Search component duplicates the exact same filtering logic already implemented in memoizedFilter in OptionsList component. This code duplication violates DRY principle and creates a maintenance burden - any changes to the filtering algorithm need to be made in two places. Consider extracting this logic to a shared utility function or having Search component use the same filtering logic from OptionsList.
        const filterOptions = (options, filterValue) => {
            if (!filterValue) return options;
            
            return options.filter(option => {
                // Handle primitive values directly
                const isPrimitive = typeof option !== 'object' || option === null;
                if (isPrimitive) {
                    const normalizedOption = option
                        .toString()
                        .normalize('NFD')
                        .replace(/[\u0300-\u036f]/g, '')
                        .toLowerCase();
                    const normalizedFilter = filterValue
                        .normalize('NFD')
                        .replace(/[\u0300-\u036f]/g, '')
                        .toLowerCase();
                    return normalizedOption.includes(normalizedFilter);
                } else {
                    // For objects, use the existing search logic
                    const searchByFields = searchState.value?.searchBy?.length
                        ? searchState.value?.searchBy
                        : Object.keys(option);
                    return searchByFields.some(key => {
                        const optionValue = option[key];
                        if (!optionValue) return false;
                        const normalizedOption = optionValue
                            .toString()
                            .normalize('NFD')
                            .replace(/[\u0300-\u036f]/g, '')
                            .toLowerCase();
                        const normalizedFilter = filterValue
                            .normalize('NFD')
                            .replace(/[\u0300-\u036f]/g, '')
                            .toLowerCase();

                        return normalizedOption.includes(normalizedFilter);
                    });
                }
            });
        };

src/wwElement_Trigger.vue:335

  • The fallback value 'Selected: {count}' is hardcoded in English. If props.content.displayText is undefined or null (e.g., in an older configuration without this property), the component will always display English text, breaking internationalization for other languages. Consider using a proper i18n key or ensuring the default value from ww-config.js is always present.
            const template = wwLib.wwLang.getText(props.content.displayText) || 'Selected: {count}';

src/wwElement_Search.vue:101

  • There's a potential race condition in searchBy usage. filterOptions (line 100) uses searchState.value?.searchBy from the previous updateSearch call, but the new searchBy is passed to updateSearch immediately after (line 101). If searchBy changes between calls, the searchMatches will be computed using the old searchBy but stored with the new searchBy, causing inconsistency. Consider passing searchBy directly to filterOptions instead of relying on searchState.value?.searchBy, or use the searchBy parameter from debouncedUpdateSearch directly in the filtering logic.
        const debouncedUpdateSearch = debounce((value, searchBy) => {
            if (updateSearch) {
                // Compute searchMatches here in the Search component
                const searchMatches = value ? filterOptions(options.value, value) : [];
                updateSearch({ value, searchBy, searchMatches });

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

// Apply sorting if sortSelectedToTop is enabled
if (props.content.sortSelectedToTop && selectedValue.value != null) {
try {
isSorting.value = true;

Copilot AI Feb 23, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The isSorting flag is being set within a computed property, which violates Vue's reactivity principles. Computed properties should be pure functions without side effects. Setting isSorting.value inside filteredOptions computed will cause it to run during every reactivity cycle, potentially triggering the setTimeout multiple times. This should be moved to a watcher or a method that is called when sorting is actually needed.

Copilot uses AI. Check for mistakes.
Comment thread src/wwElement_Trigger.vue
<div v-else :style="triggerStyle">
<div v-if="isOptionSelected" class="ww-input-select__chip_container">
<!-- Display Chips Mode -->
<div v-if="isOptionSelected && content.displayMode === 'chips'" class="ww-input-select__chip_container">

Copilot AI Feb 23, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The displayMode check doesn't handle the case where content.displayMode is undefined or null (backward compatibility for older configurations). When displayMode is undefined, the condition 'content.displayMode === "chips"' will be false, and 'content.displayMode === "count"' will also be false, so the component will fall through to showing the placeholder even when options are selected. Consider using 'content.displayMode !== "count"' for the chips condition to maintain backward compatibility with existing components that don't have displayMode set.

Suggested change
<div v-if="isOptionSelected && content.displayMode === 'chips'" class="ww-input-select__chip_container">
<div v-if="isOptionSelected && content.displayMode !== 'count'" class="ww-input-select__chip_container">

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants