Skip to content

Commit d2471fc

Browse files
committed
Working range selection without edge cases
1 parent a2a4010 commit d2471fc

6 files changed

Lines changed: 124 additions & 33 deletions

File tree

dev/vscode-list.html

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -23,61 +23,61 @@ <h2 class="story-title">Basic example</h2>
2323
<div class="story-content">
2424
<component-preview>
2525
<input type="text">
26-
<vscode-list arrows indent="16" id="basic-example">
26+
<vscode-list arrows indent="16" id="basic-example" multi-select>
2727
<vscode-list-item>
2828
<vscode-icon name="folder" slot="icon-branch"></vscode-icon>
2929
<vscode-icon name="folder-opened" slot="icon-branch-opened"></vscode-icon>
3030
<vscode-icon name="file" slot="icon-leaf"></vscode-icon>
3131
lorem
3232
</vscode-list-item>
33-
<vscode-list-item>
33+
<vscode-list-item open>
3434
<vscode-icon name="folder" slot="icon-branch"></vscode-icon>
3535
<vscode-icon name="folder-opened" slot="icon-branch-opened"></vscode-icon>
3636
<vscode-icon name="file" slot="icon-leaf"></vscode-icon>
3737
ipsum
38-
<vscode-list-item slot="children">
38+
<vscode-list-item>
3939
<vscode-icon name="folder" slot="icon-branch"></vscode-icon>
4040
<vscode-icon name="folder-opened" slot="icon-branch-opened"></vscode-icon>
4141
<vscode-icon name="file" slot="icon-leaf"></vscode-icon>
4242
lorem
4343
</vscode-list-item>
44-
<vscode-list-item slot="children">
44+
<vscode-list-item>
4545
<vscode-icon name="folder" slot="icon-branch"></vscode-icon>
4646
<vscode-icon name="folder-opened" slot="icon-branch-opened"></vscode-icon>
4747
<vscode-icon name="file" slot="icon-leaf"></vscode-icon>
4848
ipsum
4949
</vscode-list-item>
50-
<vscode-list-item closed>
50+
<vscode-list-item>
5151
<vscode-icon name="folder" slot="icon-branch"></vscode-icon>
5252
<vscode-icon name="folder-opened" slot="icon-branch-opened"></vscode-icon>
5353
<vscode-icon name="file" slot="icon-leaf"></vscode-icon>
5454
dolor
55-
<vscode-list-item slot="children">
55+
<vscode-list-item>
5656
<vscode-icon name="folder" slot="icon-branch"></vscode-icon>
5757
<vscode-icon name="folder-opened" slot="icon-branch-opened"></vscode-icon>
5858
<vscode-icon name="file" slot="icon-leaf"></vscode-icon>
5959
lorem
6060
</vscode-list-item>
61-
<vscode-list-item slot="children">
61+
<vscode-list-item>
6262
<vscode-icon name="folder" slot="icon-branch"></vscode-icon>
6363
<vscode-icon name="folder-opened" slot="icon-branch-opened"></vscode-icon>
6464
<vscode-icon name="file" slot="icon-leaf"></vscode-icon>
6565
ipsum
66-
<vscode-list-item slot="children">
66+
<vscode-list-item>
6767
<vscode-icon name="folder" slot="icon-branch"></vscode-icon>
6868
<vscode-icon name="folder-opened" slot="icon-branch-opened"></vscode-icon>
6969
<vscode-icon name="file" slot="icon-leaf"></vscode-icon>
7070
lorem
7171
</vscode-list-item>
72-
<vscode-list-item slot="children">
72+
<vscode-list-item>
7373
<vscode-icon name="folder" slot="icon-branch"></vscode-icon>
7474
<vscode-icon name="folder-opened" slot="icon-branch-opened"></vscode-icon>
7575
<vscode-icon name="file" slot="icon-leaf"></vscode-icon>
7676
lorem
7777
</vscode-list-item>
7878
</vscode-list-item>
7979
</vscode-list-item>
80-
<vscode-list-item slot="children">
80+
<vscode-list-item>
8181
<vscode-icon name="folder" slot="icon-branch"></vscode-icon>
8282
<vscode-icon name="folder-opened" slot="icon-branch-opened"></vscode-icon>
8383
<vscode-icon name="file" slot="icon-leaf"></vscode-icon>
@@ -95,7 +95,7 @@ <h2 class="story-title">Basic example</h2>
9595
</div>
9696
</div>
9797

98-
<div class="story">
98+
<!-- <div class="story">
9999
<h2 class="story-title">Description example</h2>
100100
<div class="story-content">
101101
<component-preview>
@@ -109,9 +109,9 @@ <h2 class="story-title">Description example</h2>
109109
</vscode-list>
110110
</component-preview>
111111
</div>
112-
</div>
112+
</div> -->
113113

114-
<div class="story">
114+
<!-- <div class="story">
115115
<h2 class="story-title">Decorations, actions</h2>
116116
<div class="story-content">
117117
<component-preview>
@@ -133,7 +133,7 @@ <h2 class="story-title">Decorations, actions</h2>
133133
</vscode-list>
134134
</component-preview>
135135
</div>
136-
</div>
136+
</div> -->
137137

138138
</main>
139139
</body>

src/vscode-list-item/vscode-list-item.styles.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -130,9 +130,13 @@ const styles: CSSResultGroup = [
130130
margin-left: 4px;
131131
}
132132
133-
:host([closed]) ::slotted(vscode-list-item) {
133+
:host([branch]) ::slotted(vscode-list-item) {
134134
display: none;
135135
}
136+
137+
:host([branch][open]) ::slotted(vscode-list-item) {
138+
display: block;
139+
}
136140
`,
137141
];
138142

src/vscode-list-item/vscode-list-item.ts

Lines changed: 60 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,11 @@ import {VscElement} from '../includes/VscElement';
1010
import styles from './vscode-list-item.styles';
1111
import {classMap} from 'lit/directives/class-map.js';
1212
import {listContext, type ListContext} from '../vscode-list/list-context';
13-
import {initPathTrackerProps} from '../vscode-list/helpers';
13+
import {
14+
findAncestorOnSpecificLevel,
15+
initPathTrackerProps,
16+
selectItemAndAllVisibleDescendants,
17+
} from '../vscode-list/helpers';
1418

1519
const BASE_INDENT = 3;
1620
const ARROW_CONTAINER_WIDTH = 30;
@@ -36,13 +40,20 @@ export class VscodeListItem extends VscElement {
3640
branch = false;
3741

3842
@property({type: Boolean, reflect: true})
39-
closed = false;
43+
open = false;
4044

4145
@property({type: Number, reflect: true})
4246
level = 0;
4347

4448
@property({type: Boolean, reflect: true})
45-
selected = false;
49+
set selected(val: boolean) {
50+
this._selected = val;
51+
this._listContextState.selectedItems.add(this);
52+
}
53+
get selected(): boolean {
54+
return this._selected;
55+
}
56+
private _selected = false;
4657

4758
@property({type: Number, reflect: true})
4859
tabIndex = -1;
@@ -51,8 +62,10 @@ export class VscodeListItem extends VscElement {
5162
private _listContextState: ListContext = {
5263
arrows: false,
5364
indent: 8,
65+
multiSelect: false,
5466
selectedItems: new Set(),
5567
focusedItem: null,
68+
prevFocusedItem: null,
5669
focusItem: () => {
5770
return;
5871
},
@@ -96,6 +109,37 @@ export class VscodeListItem extends VscElement {
96109
}
97110
}
98111

112+
private _selectRange() {
113+
const prevFocused = this._listContextState.prevFocusedItem;
114+
115+
if (!prevFocused || prevFocused === this) {
116+
return;
117+
}
118+
119+
const prevFocusedLevel = +(prevFocused.dataset.level ?? '');
120+
const focusedLevel = +(this.dataset.level ?? '');
121+
122+
let closestAncestor: VscodeListItem | null;
123+
124+
if (focusedLevel > prevFocusedLevel) {
125+
closestAncestor = findAncestorOnSpecificLevel(this, prevFocusedLevel);
126+
} else if (focusedLevel < prevFocusedLevel) {
127+
closestAncestor = findAncestorOnSpecificLevel(prevFocused, focusedLevel);
128+
} else {
129+
closestAncestor = prevFocused;
130+
}
131+
132+
const from = +(closestAncestor?.dataset.index ?? '');
133+
const to = +(this.dataset.index ?? '');
134+
135+
for (let i = from; i <= to; i++) {
136+
const li = this.parentElement?.querySelector(
137+
`:scope > [data-index="${i}"]`
138+
) as VscodeListItem;
139+
selectItemAndAllVisibleDescendants(li);
140+
}
141+
}
142+
99143
private _mainSlotChange() {
100144
this._initiallyAssignedListItems.forEach((li) => {
101145
li.setAttribute('slot', 'children');
@@ -120,6 +164,8 @@ export class VscodeListItem extends VscElement {
120164
this._listContextState.focusedItem !== this
121165
) {
122166
this._listContextState.focusedItem.tabIndex = -1;
167+
this._listContextState.prevFocusedItem =
168+
this._listContextState.focusedItem;
123169
this._listContextState.focusedItem = null;
124170
}
125171

@@ -130,10 +176,17 @@ export class VscodeListItem extends VscElement {
130176
ev.stopPropagation();
131177

132178
const isCtrlDown = ev.ctrlKey;
179+
const isShiftDown = ev.shiftKey;
180+
181+
if (isShiftDown) {
182+
this._selectRange();
183+
return;
184+
}
185+
133186
this._selectItem(isCtrlDown);
134187

135188
if (this.branch && !(this._listContextState.multiSelect && isCtrlDown)) {
136-
this.closed = !this.closed;
189+
this.open = !this.open;
137190
}
138191

139192
this._focusItem(this);
@@ -152,12 +205,6 @@ export class VscodeListItem extends VscElement {
152205
this.removeEventListener('focus', this._handleComponentFocus);
153206
}
154207

155-
willUpdate(changedProperties: PropertyValues<this>): void {
156-
if (changedProperties.has('selected') && this.selected) {
157-
this._listContextState.selectedItems.add(this);
158-
}
159-
}
160-
161208
render(): TemplateResult {
162209
const {arrows, indent, hasBranchItem} = this._listContextState;
163210
let indentation = BASE_INDENT + this.level * indent;
@@ -176,18 +223,18 @@ export class VscodeListItem extends VscElement {
176223
? html`<div
177224
class=${classMap({
178225
'arrow-container': true,
179-
'icon-rotated': !this.closed,
226+
'icon-rotated': this.open,
180227
})}
181228
>
182229
${arrowIcon}
183230
</div>`
184231
: nothing}
185232
<div class="icon-container">
186233
<slot name="icon"></slot>
187-
${this.branch && this.closed
234+
${this.branch && !this.open
188235
? html`<slot name="icon-branch"></slot>`
189236
: nothing}
190-
${this.branch && !this.closed
237+
${this.branch && this.open
191238
? html`<slot name="icon-branch-opened"></slot>`
192239
: nothing}
193240
${!this.branch ? html`<slot name="icon-leaf"></slot>` : nothing}

src/vscode-list/helpers.ts

Lines changed: 42 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ export const initPathTrackerProps = (
2424

2525
items.forEach((item, i) => {
2626
item.level = parentElementLevel + 1;
27+
item.dataset.level = (parentElementLevel + 1).toString();
2728
item.dataset.index = i.toString();
2829
item.dataset.last = i === numChildren - 1 ? 'true' : 'false';
2930
});
@@ -38,7 +39,7 @@ export const findLastChildItem = (item: VscodeListItem): VscodeListItem => {
3839

3940
const lastItem = children[children.length - 1];
4041

41-
if (lastItem.branch && !lastItem.closed) {
42+
if (lastItem.branch && lastItem.open) {
4243
return findLastChildItem(lastItem);
4344
} else {
4445
return lastItem;
@@ -66,7 +67,7 @@ export const findClosestParentHasNextSibling = (
6667
};
6768

6869
export const findNextItem = (item: VscodeListItem): VscodeListItem | null => {
69-
if (item.branch && !item.closed) {
70+
if (item.branch && item.open) {
7071
return item.querySelector<VscodeListItem>('vscode-list-item');
7172
}
7273

@@ -127,9 +128,47 @@ export const findPrevItem = (item: VscodeListItem): VscodeListItem | null => {
127128
}
128129
}
129130

130-
if (prevSibling && prevSibling.branch && !prevSibling.closed) {
131+
if (prevSibling && prevSibling.branch && prevSibling.open) {
131132
return findLastChildItem(prevSibling);
132133
}
133134

134135
return prevSibling;
135136
};
137+
138+
export const findAncestorOnSpecificLevel = (
139+
item: VscodeListItem,
140+
level: number
141+
): VscodeListItem | null => {
142+
console.log(level);
143+
if (
144+
!item.parentElement ||
145+
item.parentElement.tagName.toUpperCase() !== 'VSCODE-LIST-ITEM'
146+
) {
147+
return null;
148+
}
149+
150+
const parent = item.parentElement as VscodeListItem;
151+
const itemLevel = +(item.dataset.level ?? '');
152+
153+
if (itemLevel > level) {
154+
return findAncestorOnSpecificLevel(parent, level);
155+
}
156+
157+
if (itemLevel === level) {
158+
return item;
159+
}
160+
161+
return null;
162+
};
163+
164+
export const selectItemAndAllVisibleDescendants = (item: VscodeListItem) => {
165+
item.selected = true;
166+
167+
if (item.branch && item.open) {
168+
const children = item.querySelectorAll<VscodeListItem>(':scope > vscode-list-item');
169+
170+
children.forEach((c) => {
171+
selectItemAndAllVisibleDescendants(c);
172+
});
173+
}
174+
};

src/vscode-list/list-context.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ export interface ListContext {
88
multiSelect: boolean;
99
selectedItems: Set<VscodeListItem>;
1010
focusedItem: VscodeListItem | null;
11+
prevFocusedItem: VscodeListItem | null;
1112
focusItem: (item: VscodeListItem) => void;
1213
/** If arrows are visible and `List` component has not any branch item, the
1314
* extra padding should be removed in the leaf elements before the content

src/vscode-list/vscode-list.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ export class VscodeList extends VscElement {
5555
multiSelect: false,
5656
selectedItems: new Set(),
5757
focusedItem: null,
58+
prevFocusedItem: null,
5859
hasBranchItem: false,
5960
rootElement: this,
6061
focusItem: this._focusItem,
@@ -145,10 +146,9 @@ export class VscodeList extends VscElement {
145146
);
146147

147148
focusedItem.selected = true;
148-
this._listContextState.selectedItems.add(focusedItem);
149149

150150
if (focusedItem.branch) {
151-
focusedItem.closed = !focusedItem.closed;
151+
focusedItem.open = !focusedItem.open;
152152
}
153153
}
154154
}

0 commit comments

Comments
 (0)