Skip to content

feat: Drag-Fill capability and Hybrid SelectionModel plugin#1143

Merged
6pac merged 13 commits into
masterfrom
dragfill
Oct 27, 2025
Merged

feat: Drag-Fill capability and Hybrid SelectionModel plugin#1143
6pac merged 13 commits into
masterfrom
dragfill

Conversation

@6pac

@6pac 6pac commented Aug 30, 2025

Copy link
Copy Markdown
Owner

Take 2!!

New HybridSelectionModel combines cell and row selection models. It functions like CellSelectionModel except when certain conditions switch it into RowSelectionModel.
If the options.rowSelectOverride function exists, then HybridSelectionModel calls it expecting a boolean return to determine whether to behave like RowSelectionModel.
If this option is not set, then selection of a column that is a member of the array options.rowSelectColumnObjectArr will be treated with the behaviour of RowSelectionModel.

Drag and Replace option mirrors Excel as closely as possible. When a range of cells is selected, a grey box appears at the bottom right of the selection area, and dragging that grey box expands the selection area and replaces the newly selected cells will the contents of the existing cells. Excel specifically only allows expansion horizontally or vertically, but here we allow both. The current behaviour may be modified.
Note that the spreadsheet example the FormulaEditor causes problems with this new feature because it creates a new CellRangeSelector each time a cell is edited. This editor needs to add an event to the existing grid's CellRangeSelector, or create a new one.

@6pac

6pac commented Aug 30, 2025

Copy link
Copy Markdown
Owner Author

SlickPR_DragFill_Hybrid

@6pac

6pac commented Aug 30, 2025

Copy link
Copy Markdown
Owner Author

Note that as part of the commits I git checkout dist/* so that there is no noise from rebuilds. This means using the dist/* files as-is after the commit will not include the changes just made. I assume that the CI process rebuilds dist/* so there is no need for me to update it. If these files are not rebuilt, then the examples may not work.

Comment thread src/models/gridEvents.interface.ts Outdated
Comment thread src/plugins/slick.hybridselectionmodel.ts Outdated
@ghiscoding

Copy link
Copy Markdown
Collaborator

It's quite interesting and promising, so far I replicated these changes in Slickgrid-Universal and gave it a try locally, with only 1 example, and it's quite cool to see in action. However, I'm not sure if it's possible at all, but from being an Excel user. When we make a larger selection then all is good, but when we take that large selection and want to shrink it then it's not behaving like Excel because in Excel it would shrink (as shown below) but in SlickGrid it seems to rather start a new selection which was a bit confusing at first. I understand that this might not be doable at all, but it's just the first observation that caught my eyes at first (see below).

Apart from that, I haven't had a chance to look at the code that much, just some quick glance at it while implementing it in my own libs. I'll have to look at the code more closely when I get more time. Is there a reason why this was not created in 2 separate PRs? I mean there's Hybrid Selection Model and Drag-Fill, do they have to come together? Just curious...

Also how does the drag-fill works? Because in Excel, it looks at the existing data to guess what the next cell values will be, is that what you wanted to do? Would the code implementation fall in the plugin or in the example? I like this new plugin and the hybrid mode, however I don't want to increase the lib too much so maybe that drag-fill is for example code?

EXCEL_azNmak1XPe

@6pac

6pac commented Aug 31, 2025

Copy link
Copy Markdown
Owner Author

It's clearer if you use the new CSS because the drag-fill border can be different to the original selection border.

I created this in one update because there were a number of updates to core code that were for both and it was hard to separate them out, and last PR when I added functions to support future features you were asking 'why is that there?'.

I could go back and do that if you need. Most of the changes for the HybridSelectionModel (other than the plugin itself) were just routing a few extra parameters through the event paths in the core code.

The drag-fill function by default is in a DragHelper object in Slick.Core. Drag-fill has never been framed as a plugin, but with the new OnDragReplaceCells event, a custom drag-fill function can easily be used. There are a lot of requests for spreadsheet like behaviour, but everyone wants different behavior.
I've never tried to drag-shrink in Excel, but I'm sure that could be accommodated.

I did feel a little awkward in putting the default drag-fill function in core code, but I did think it was good to have something. We could just ship with nothing, and add the default implementation to the example as you suggest. I'd be fine with that. But I think everything else is worth adding - again, a lot of it is just adding extra params to event flows, plus some logic to support the drag-select as a first class mode as distinct from initial select.

There were also some other minor fixes to annoying bugs where values entered into cells would disappear unpredictably. This was because the editor was not committing before the drag.

This whole PR was really hard and time consuming to do, because it's all mouse based, so you have to log everything - no using debug tools.

@ghiscoding

ghiscoding commented Aug 31, 2025

Copy link
Copy Markdown
Collaborator

I could go back and do that if you need. Most of the changes for the HybridSelectionModel (other than the plugin itself) were just routing a few extra parameters through the event paths in the core code.

No no, it's all good, I was mainly asking out of curiosity to know if they were working hand in hand and it looks like they do.

We could just ship with nothing, and add the default implementation to the example as you suggest. I'd be fine with that.

Yeah I think that I would prefer that since, as you said, every users might have a different idea of what the drag-fill could and should do... so to add it in the example and let the user decide what logic he wants to follow is making more sense to me.

This whole PR was really hard and time consuming to do, because it's all mouse based, so you have to log everything - no using debug tools.

Yup, been there done that... I pretty much had to do the same when I replaced the jQuery mousewheel with our custom Slick.Interactions core file. BTW, I assume you know that there's still some console logs left in the code? Looking good so far, but like I said, I would need to spend more time to look at the code eventually. Cheers

@6pac

6pac commented Sep 4, 2025

Copy link
Copy Markdown
Owner Author

@ghiscoding the CI build seems to be bailing at a certain number of errors, so I fix what it lists and then get a new bunch. Is there a way to get it to list all the errors? I had a look at the CI build YML but it was not obvious.

@6pac

6pac commented Sep 4, 2025

Copy link
Copy Markdown
Owner Author

@ghiscoding looks like that's OK now.

Also, I went to move the cell-copy code out of SelectionUtils (in slick.core) and realised there are quite a few helper functions in there.
I'd have to move verticalTargetRange, horizontalTargetRange, defaultCopyDraggedCellRange and copyCellsToTargetRange. I think they are all kind of useful, and now I'm leaning towards thinking they should be left in.

Even if someone was putting together their own custom copy code, I can see them using the helper functions.

Opinions?

@ghiscoding

ghiscoding commented Sep 4, 2025

Copy link
Copy Markdown
Collaborator

@ghiscoding looks like that's OK now.

Also, I went to move the cell-copy code out of SelectionUtils (in slick.core) and realised there are quite a few helper functions in there. I'd have to move verticalTargetRange, horizontalTargetRange, defaultCopyDraggedCellRange and copyCellsToTargetRange. I think they are all kind of useful, and now I'm leaning towards thinking they should be left in.

Even if someone was putting together their own custom copy code, I can see them using the helper functions.

Opinions?

I would say that if the utils are required for the feature to work then they're fine in Slick core. However if it's only useful or specific for the examples to work then that might be better to move them in the example(s)... I'm assuming that it's probably the first option, so in core seems fine to me. The only thing I would say is if there's any code that seems repetitive, then trying to merge some of the code might be good but only when possible.

I'm not sure if the changes are related to the Cypress failing tests? I'm re-running the CI, we'll wait and see

this.init = function () {
navOnLR = args.grid.getOptions().editorCellNavOnLRKeys;
input = Slick.Utils.createDomElement('input', { type: 'text', className: 'editor-text' }, args.container);
input.addEventListener("keydown.nav", navOnLR ? handleKeydownLRNav : handleKeydownLRNoNav);

@ghiscoding ghiscoding Sep 6, 2025

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I'm trying to copy more of your code on my end and I found that this code isn't being executed at all because keydown.nav is not a valid JS event. For the history side of things, this kind of code came from when we were using jQuery which allowed passing namespace (e.g. keydown.suffix) but that won't work in pure JS since it only knows keydown and has no idea what keydown.nav is... so just removing the suffix should be good enough, even if in this case with/without the event doesn't seem to change much (do we even need that code at all?)

BTW, do you know what the editorCellNavOnLRKeys grid option does exactly? I never used it myself and don't have code that uses this option either..

Comment thread src/slick.core.ts Outdated
}


public static defaultCopyDraggedCellRange(e , args) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Suggested change
public static defaultCopyDraggedCellRange(e , args) {
public static defaultCopyDraggedCellRange(_e: SlickEventData<any>, args: OnDragReplaceCellsEventArgs): void {

Comment thread src/slick.core.ts Outdated
Comment on lines +421 to +423
const dragReplaceEl = document.getElementById(this.id);
if (dragReplaceEl) { dragReplaceEl.remove(); }
//console.log('DragReplaceEl.removeEl');

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Suggested change
const dragReplaceEl = document.getElementById(this.id);
if (dragReplaceEl) { dragReplaceEl.remove(); }
//console.log('DragReplaceEl.removeEl');
document.getElementById(this.id)?.remove();

Comment thread src/slick.grid.ts Outdated
Comment on lines +4048 to +4051
if (this.currentEditor) {
const commitSuccess = this.getEditorLock().commitCurrentEdit();
if (!commitSuccess) { return false; }
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Suggested change
if (this.currentEditor) {
const commitSuccess = this.getEditorLock().commitCurrentEdit();
if (!commitSuccess) { return false; }
}
if (this.currentEditor && !this.getEditorLock().commitCurrentEdit()) {
return false;
}

Comment thread src/slick.core.ts Outdated
@6pac

6pac commented Sep 11, 2025

Copy link
Copy Markdown
Owner Author

@ghiscoding sorry this is taking a while. I went back to change the default drag-fill function and have found a reasonably complex bug that I need to fix. Been too busy last week or so. I'll take all the suggestions on board - they are not blocking the process.

@ghiscoding

ghiscoding commented Sep 11, 2025

Copy link
Copy Markdown
Collaborator

no problem at all, better take more time to get it right... Also the suggestions are just suggestions, you can click the "Commit Suggestion" if you want and that would push it as a commit. I merged only the 1 suggestion that fixed the Cypress failing tests (always glad to have so many tests, it often catches real issues as it did in this PR, hence the suggestion that I committed to fix it)

I also added a bunch of Cypress tests on my end, I can copy them over in here too, that is when the implementation is finalized

@6pac

6pac commented Oct 25, 2025

Copy link
Copy Markdown
Owner Author

OK, this PR is now complete. I have added your suggestions and kept only helper methods in the core lib, the actual copy function has been shifted to the example.
I have finally worked out how to add the ongoing changes to master back to my PR branch without them being part of the commit history, however it rewrites history and requires a force push. I think all of my actual commits are untouched, but a few of the Cypress tests are failing, and I seem to recall that you fixed them. That fix may need to be reapplied.

Comment thread src/slick.core.ts Outdated
@ghiscoding

ghiscoding commented Oct 25, 2025

Copy link
Copy Markdown
Collaborator

@6pac I had to compare against my own PR duplicate to find it, but yeah I found it and proposed and merged the commit suggestion and all Cypress tests are passing... Now I will try to update my own PR of the same feature but it might harder now that you did a force push which does rewrite history, but that's ok I'll figure it out :D

However while looking at the CI running, I see that the build has a few typing issues (4x), you should think about fixing them before merging the PR. On my end, Perhaps you can change matchClassTag to be optional?

> tsc --emitDeclarationOnly --incremental --declarationMap --outDir dist/types
src/slick.interactions.ts:113:55 - error TS2345: Argument of type '(e: DragEvent, dd: DragPosition) => boolean | void' is not assignable to parameter of type '(e: DragEvent, dd: DragItem) => boolean | void'.
  Types of parameters 'dd' and 'dd' are incompatible.
    Property 'matchClassTag' is missing in type 'DragItem' but required in type 'DragPosition'.
113         const result = executeDragCallbackWhenDefined(onDragInit as (e: DragEvent, dd: DragPosition) => boolean | void, event, originaldd as DragItem);
                                                          ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  src/models/drag.interface.ts:18:3
    18   matchClassTag: string;
         ~~~~~~~~~~~~~
    'matchClassTag' is declared here.
src/slick.interactions.ts:136:40 - error TS2345: Argument of type '((e: DragEvent, dd: DragPosition) => boolean | void) | undefined' is not assignable to parameter of type '((e: DragEvent, dd: DragItem) => boolean | void) | undefined'.
  Type '(e: DragEvent, dd: DragPosition) => boolean | void' is not assignable to type '(e: DragEvent, dd: DragItem) => boolean | void'.
    Types of parameters 'dd' and 'dd' are incompatible.
      Property 'matchClassTag' is missing in type 'DragItem' but required in type 'DragPosition'.
136         executeDragCallbackWhenDefined(onDragStart, event, originaldd as DragItem);
                                           ~~~~~~~~~~~
  src/models/drag.interface.ts:18:3
    18   matchClassTag: string;
         ~~~~~~~~~~~~~
    'matchClassTag' is declared here.
src/slick.interactions.ts:141:38 - error TS2345: Argument of type '((e: DragEvent, dd: DragPosition) => boolean | void) | undefined' is not assignable to parameter of type '((e: DragEvent, dd: DragItem) => boolean | void) | undefined'.
  Type '(e: DragEvent, dd: DragPosition) => boolean | void' is not assignable to type '(e: DragEvent, dd: DragItem) => boolean | void'.
    Types of parameters 'dd' and 'dd' are incompatible.
      Property 'matchClassTag' is missing in type 'DragItem' but required in type 'DragPosition'.
141       executeDragCallbackWhenDefined(onDrag, event, originaldd as DragItem);
                                         ~~~~~~
  src/models/drag.interface.ts:18:3
    18   matchClassTag: string;
         ~~~~~~~~~~~~~
    'matchClassTag' is declared here.
src/slick.interactions.ts:156:38 - error TS2345: Argument of type '((e: DragEvent, dd: DragPosition) => boolean | void) | undefined' is not assignable to parameter of type '((e: DragEvent, dd: DragItem) => boolean | void) | undefined'.
  Type '(e: DragEvent, dd: DragPosition) => boolean | void' is not assignable to type '(e: DragEvent, dd: DragItem) => boolean | void'.
    Types of parameters 'dd' and 'dd' are incompatible.
      Property 'matchClassTag' is missing in type 'DragItem' but required in type 'DragPosition'.
156       executeDragCallbackWhenDefined(onDragEnd, event, originaldd as DragItem);
                                         ~~~~~~~~~
  src/models/drag.interface.ts:18:3
    18   matchClassTag: string;
         ~~~~~~~~~~~~~
    'matchClassTag' is declared here.
Found 4 errors in the same file, starting at: src/slick.interactions.ts:113

return this._selectionMode;
}

setSelectionMode(mode: string) {

@ghiscoding ghiscoding Oct 25, 2025

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

these 2 functions getSelectionMode() and setSelectionMode() aren't used, you can probably remove them?

return this._currentlySelectedRange;
}

getPreviousRange() {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

this is also not used

@6pac

6pac commented Oct 25, 2025

Copy link
Copy Markdown
Owner Author

Thanks, will check these out.
The ongoing issue I have with PRs is that master commits happen over the month or so I'm working on them, and then I can no longer push until I integrate the changes to master. However when I do that, I end up with a whole bunch of stuff in my PR that's nothing to do with the PR, it's just changes to master.
I have tried a few suggestions, and this seems to work:

get checkout [branch]
git fetch origin
git rebase origin/master
git push origin [branch] --force-with-lease

The force push does rewrite history, but I'm not sure how or where, since my actual PR commits remain unchanged and the changes to master a applied 'silently'.
Clearly it does lose changes made by other people (like your Cypress changes), but I suppose I could possibly pull just those commits, rather than the general commits to master.

This is way better than what I was doing before, but it's still not ideal. Let me know if you know of a better way.

@ghiscoding

ghiscoding commented Oct 25, 2025

Copy link
Copy Markdown
Collaborator

Yeah the git force is the way to go, it's all good I was able to replicate on my end. I purposely left 2 other PRs open to avoid conflicting with your PR, I'll merge them after your PR is merged.

I try not to make too many git force push but when I do it via these 3 lines

git rebase origin/master
# fix all conflicts then add all git changes
git add . 
git push origin HEAD --force

It's exciting to see this is going to get merged soon :)

@6pac

6pac commented Oct 27, 2025

Copy link
Copy Markdown
Owner Author

All good to merge?

@ghiscoding

Copy link
Copy Markdown
Collaborator

sure go ahead, though you might want to address the last comment I left here, otherwise it's all good, you can merge when you're ready. I'll merge my own PRs in a day or two.
#1143 (review)

You should also merge this PR before your other small PR just to avoid merge conflicts, Cheers

@6pac

6pac commented Oct 27, 2025

Copy link
Copy Markdown
Owner Author

thanks. getSelectionMode(), setSelectionMode() and getPreviousRange() are not used but are good to have in the API.

@6pac 6pac merged commit 0bd9277 into master Oct 27, 2025
3 checks passed
@6pac 6pac deleted the dragfill branch October 27, 2025 01:48
@ghiscoding

Copy link
Copy Markdown
Collaborator

@6pac great, now I can proceed with some of my changes, I got couple more to do after that too and once I'm done I'll push a new release by the end of the week. Thanks

@ghiscoding

Copy link
Copy Markdown
Collaborator

@6pac can we renamed rowSelectColumnIdArr to rowSelectColumnIds? I think the updated name is better, at least that is what I will personally use in my own Slickgrid-Universal. I know arr was a convenient name in the past, but now that we have TypeScript and intellisense, I think we can simply use rowSelectColumnIds and get to know that it's a string array via intellisense, so... would that be ok to updated the prop name?

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