Skip to content

Commit 3bafe3c

Browse files
authored
NavigatorCard component refactor (#780)
* [rdar://120048809] feat: NavigatorCard component refactor
1 parent e6bb79b commit 3bafe3c

File tree

7 files changed

+250
-185
lines changed

7 files changed

+250
-185
lines changed

src/components/Navigator/NavigatorCard.vue

Lines changed: 9 additions & 150 deletions
Original file line numberDiff line numberDiff line change
@@ -98,11 +98,12 @@
9898
<div class="input-wrapper">
9999
<FilterInput
100100
v-model="filter"
101-
:tags="availableTags"
101+
:tags="suggestedTags"
102102
:translatableTags="translatableTags"
103-
:selected-tags.sync="selectedTagsModelValue"
103+
:selected-tags.sync="selectedTags"
104104
:placeholder="$t('filter.title')"
105105
:should-keep-open-on-blur="false"
106+
:shouldTruncateTags="shouldTruncateTags"
106107
:position-reversed="!renderFilterOnTop"
107108
:clear-filter-on-tag-select="false"
108109
class="filter-component"
@@ -133,8 +134,10 @@ import BaseNavigatorCard from 'docc-render/components/Navigator/BaseNavigatorCar
133134
import { TopicTypes } from 'docc-render/constants/TopicTypes';
134135
import FilterInput from 'docc-render/components/Filter/FilterInput.vue';
135136
import keyboardNavigation from 'docc-render/mixins/keyboardNavigation';
137+
import filteredChildrenMixin from 'theme/mixins/navigator/filteredChildren';
138+
import tagsProvider from 'theme/mixins/navigator/tagsProvider';
139+
import { FILTER_TAGS, CHANGES_TAGS } from 'docc-render/constants/Tags';
136140
import { isEqual, last } from 'docc-render/utils/arrays';
137-
import { ChangeNames, ChangeNameToType } from 'docc-render/constants/Changes';
138141
import {
139142
convertChildrenArrayToObject,
140143
getAllChildren,
@@ -147,43 +150,10 @@ import Badge from 'docc-render/components/Badge.vue';
147150
148151
const STORAGE_KEY = 'navigator.state';
149152
150-
const FILTER_TAGS = {
151-
sampleCode: 'sampleCode',
152-
tutorials: 'tutorials',
153-
articles: 'articles',
154-
webServiceEndpoints: 'webServiceEndpoints',
155-
};
156-
157-
const FILTER_TAGS_TO_LABELS = {
158-
[FILTER_TAGS.sampleCode]: 'Sample Code',
159-
[FILTER_TAGS.tutorials]: 'Tutorials',
160-
[FILTER_TAGS.articles]: 'Articles',
161-
[FILTER_TAGS.webServiceEndpoints]: 'Web Service Endpoints',
162-
};
163-
164-
const FILTER_LABELS_TO_TAGS = Object.fromEntries(
165-
Object
166-
.entries(FILTER_TAGS_TO_LABELS)
167-
.map(([key, value]) => [value, key]),
168-
);
169-
170-
const TOPIC_TYPE_TO_TAG = {
171-
[TopicTypes.article]: FILTER_TAGS.articles,
172-
[TopicTypes.learn]: FILTER_TAGS.tutorials,
173-
[TopicTypes.overview]: FILTER_TAGS.tutorials,
174-
[TopicTypes.resources]: FILTER_TAGS.tutorials,
175-
[TopicTypes.sampleCode]: FILTER_TAGS.sampleCode,
176-
[TopicTypes.section]: FILTER_TAGS.tutorials,
177-
[TopicTypes.tutorial]: FILTER_TAGS.tutorials,
178-
[TopicTypes.project]: FILTER_TAGS.tutorials,
179-
[TopicTypes.httpRequest]: FILTER_TAGS.webServiceEndpoints,
180-
};
181-
182153
const NO_RESULTS = 'navigator.no-results';
183154
const NO_CHILDREN = 'navigator.no-children';
184155
const ERROR_FETCHING = 'navigator.error-fetching';
185156
const ITEMS_FOUND = 'navigator.items-found';
186-
const HIDE_DEPRECATED = 'navigator.tags.hide-deprecated';
187157
188158
/**
189159
* Renders the card for a technology and it's child symbols, in the navigator.
@@ -194,13 +164,8 @@ export default {
194164
name: 'NavigatorCard',
195165
constants: {
196166
STORAGE_KEY,
197-
FILTER_TAGS,
198-
FILTER_TAGS_TO_LABELS,
199-
FILTER_LABELS_TO_TAGS,
200-
TOPIC_TYPE_TO_TAG,
201167
ERROR_FETCHING,
202168
ITEMS_FOUND,
203-
HIDE_DEPRECATED,
204169
},
205170
components: {
206171
FilterInput,
@@ -266,7 +231,7 @@ export default {
266231
},
267232
},
268233
mixins: [
269-
keyboardNavigation,
234+
keyboardNavigation, filteredChildrenMixin, tagsProvider,
270235
],
271236
data() {
272237
return {
@@ -282,7 +247,6 @@ export default {
282247
activeUID: null,
283248
lastFocusTarget: null,
284249
allNodesToggled: false,
285-
translatableTags: [HIDE_DEPRECATED],
286250
INDEX_ROOT_KEY,
287251
};
288252
},
@@ -300,76 +264,6 @@ export default {
300264
if (errorFetching) return ERROR_FETCHING;
301265
return NO_CHILDREN;
302266
},
303-
/**
304-
* Generates an array of tag labels for filtering.
305-
* Shows only tags, that have children matches.
306-
*/
307-
availableTags({
308-
selectedTags,
309-
renderableChildNodesMap,
310-
apiChangesObject,
311-
hideAvailableTags,
312-
}) {
313-
if (hideAvailableTags || selectedTags.length) return [];
314-
const apiChangesTypesSet = new Set(Object.values(apiChangesObject));
315-
const tagLabelsSet = new Set(Object.values(FILTER_TAGS_TO_LABELS));
316-
const generalTags = new Set([HIDE_DEPRECATED]);
317-
// when API changes are available, remove the `HIDE_DEPRECATED` option
318-
if (apiChangesTypesSet.size) {
319-
generalTags.delete(HIDE_DEPRECATED);
320-
}
321-
322-
const availableTags = {
323-
type: [],
324-
changes: [],
325-
other: [],
326-
};
327-
// iterate over the nodes to render
328-
for (const childID in renderableChildNodesMap) {
329-
if (!Object.hasOwnProperty.call(renderableChildNodesMap, childID)) {
330-
continue;
331-
}
332-
// if there are no more tags to iterate over, end early
333-
if (!tagLabelsSet.size && !apiChangesTypesSet.size && !generalTags.size) {
334-
break;
335-
}
336-
// extract props
337-
const { type, path, deprecated } = renderableChildNodesMap[childID];
338-
// grab the tagLabel
339-
const tagLabel = FILTER_TAGS_TO_LABELS[TOPIC_TYPE_TO_TAG[type]];
340-
const changeType = apiChangesObject[path];
341-
// try to match a tag
342-
if (tagLabelsSet.has(tagLabel)) {
343-
// if we have a match, store it
344-
availableTags.type.push(tagLabel);
345-
// remove the match, so we can end the filter early
346-
tagLabelsSet.delete(tagLabel);
347-
}
348-
if (changeType && apiChangesTypesSet.has(changeType)) {
349-
availableTags.changes.push(this.$t(ChangeNames[changeType]));
350-
apiChangesTypesSet.delete(changeType);
351-
}
352-
if (deprecated && generalTags.has(HIDE_DEPRECATED)) {
353-
availableTags.other.push(HIDE_DEPRECATED);
354-
generalTags.delete(HIDE_DEPRECATED);
355-
}
356-
}
357-
return availableTags.type.concat(availableTags.changes, availableTags.other);
358-
},
359-
selectedTagsModelValue: {
360-
get() {
361-
return this.selectedTags.map(tag => (
362-
FILTER_TAGS_TO_LABELS[tag] || this.$t(ChangeNames[tag]) || tag
363-
));
364-
},
365-
set(values) {
366-
// guard against accidental clearings
367-
if (!this.selectedTags.length && !values.length) return;
368-
this.selectedTags = values.map(label => (
369-
FILTER_LABELS_TO_TAGS[label] || ChangeNameToType[label] || label
370-
));
371-
},
372-
},
373267
filterPattern: ({ debouncedFilter }) => (!debouncedFilter
374268
? null
375269
// remove the `g` for global, as that causes bugs when matching
@@ -401,41 +295,6 @@ export default {
401295
activeIndex: ({ activeUID, nodesToRender }) => (
402296
nodesToRender.findIndex(node => node.uid === activeUID)
403297
),
404-
/**
405-
* Returns a list of the child nodes, that match the filter pattern.
406-
* @returns NavigatorFlatItem[]
407-
*/
408-
filteredChildren({
409-
hasFilter, children, filterPattern, selectedTags, apiChanges,
410-
}) {
411-
if (!hasFilter) return [];
412-
const tagsSet = new Set(selectedTags);
413-
// find children that match current filters
414-
return children.filter(({
415-
title, path, type, deprecated, deprecatedChildrenCount, childUIDs,
416-
}) => {
417-
// groupMarkers know how many children they have and how many are deprecated
418-
const isDeprecated = deprecated || deprecatedChildrenCount === childUIDs.length;
419-
// check if `title` matches the pattern, if provided
420-
const titleMatch = filterPattern ? filterPattern.test(title) : true;
421-
// check if `type` matches any of the selected tags
422-
let tagMatch = true;
423-
if (tagsSet.size) {
424-
tagMatch = tagsSet.has(TOPIC_TYPE_TO_TAG[type]);
425-
// if there are API changes and there is no tag match, try to match change types
426-
if (apiChanges && !tagMatch) {
427-
tagMatch = tagsSet.has(apiChanges[path]);
428-
}
429-
if (!isDeprecated && tagsSet.has(HIDE_DEPRECATED)) {
430-
tagMatch = true;
431-
}
432-
}
433-
// find items, that have API changes
434-
const hasAPIChanges = apiChanges ? !!apiChanges[path] : true;
435-
// make sure groupMarker's dont get matched
436-
return titleMatch && tagMatch && hasAPIChanges;
437-
});
438-
},
439298
/**
440299
* This generates a map of all the nodes we are allowed to render at a certain time.
441300
* This is used on both toggling, as well as on navigation and filtering.
@@ -505,7 +364,7 @@ export default {
505364
* @returns boolean
506365
*/
507366
deprecatedHidden: ({ selectedTags }) => (
508-
selectedTags[0] === HIDE_DEPRECATED
367+
selectedTags[0] === FILTER_TAGS.hideDeprecated
509368
),
510369
apiChangesObject() {
511370
return this.apiChanges || {};
@@ -524,7 +383,7 @@ export default {
524383
apiChanges(value) {
525384
if (value) return;
526385
// if we remove APIChanges, remove all related tags as well
527-
this.selectedTags = this.selectedTags.filter(t => !this.$t(ChangeNames[t]));
386+
this.selectedTags = this.selectedTags.filter(t => !Object.values(CHANGES_TAGS).includes(t));
528387
},
529388
async activeUID(newUid, oldUID) {
530389
// Update the dynamicScroller item's size, when we change the UID,

src/constants/Changes.js

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
/**
22
* This source file is part of the Swift.org open source project
33
*
4-
* Copyright (c) 2021-2023 Apple Inc. and the Swift project authors
4+
* Copyright (c) 2021-2024 Apple Inc. and the Swift project authors
55
* Licensed under Apache License v2.0 with Runtime Library Exception
66
*
77
* See https://swift.org/LICENSE.txt for license information
@@ -21,9 +21,3 @@ export const ChangeNames = {
2121
[ChangeTypes.added]: 'change-type.added',
2222
[ChangeTypes.deprecated]: 'change-type.deprecated',
2323
};
24-
25-
export const ChangeNameToType = {
26-
'change-type.modified': ChangeTypes.modified,
27-
'change-type.added': ChangeTypes.added,
28-
'change-type.deprecated': ChangeTypes.deprecated,
29-
};

src/constants/Tags.js

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
/**
2+
* This source file is part of the Swift.org open source project
3+
*
4+
* Copyright (c) 2023-2024 Apple Inc. and the Swift project authors
5+
* Licensed under Apache License v2.0 with Runtime Library Exception
6+
*
7+
* See https://swift.org/LICENSE.txt for license information
8+
* See https://swift.org/CONTRIBUTORS.txt for Swift project authors
9+
*/
10+
11+
import { TopicTypes } from 'docc-render/constants/TopicTypes';
12+
import { ChangeNames } from 'docc-render/constants/Changes';
13+
14+
export const TYPE_TAGS = {
15+
sampleCode: 'filter.tags.sample-code',
16+
tutorials: 'filter.tags.tutorials',
17+
articles: 'filter.tags.articles',
18+
webServiceEndpoints: 'filter.tags.web-service-endpoints',
19+
};
20+
21+
export const CHANGES_TAGS = ChangeNames;
22+
23+
export const FILTER_TAGS = {
24+
...TYPE_TAGS,
25+
...CHANGES_TAGS,
26+
hideDeprecated: 'filter.tags.hide-deprecated',
27+
};
28+
29+
export const TOPIC_TYPE_TO_TAG = {
30+
[TopicTypes.article]: FILTER_TAGS.articles,
31+
[TopicTypes.learn]: FILTER_TAGS.tutorials,
32+
[TopicTypes.overview]: FILTER_TAGS.tutorials,
33+
[TopicTypes.resources]: FILTER_TAGS.tutorials,
34+
[TopicTypes.sampleCode]: FILTER_TAGS.sampleCode,
35+
[TopicTypes.section]: FILTER_TAGS.tutorials,
36+
[TopicTypes.tutorial]: FILTER_TAGS.tutorials,
37+
[TopicTypes.project]: FILTER_TAGS.tutorials,
38+
[TopicTypes.httpRequest]: FILTER_TAGS.webServiceEndpoints,
39+
};

src/lang/locales/en-US.json

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -138,7 +138,14 @@
138138
"navigate": "To navigate the symbols, press Up Arrow, Down Arrow, Left Arrow or Right Arrow",
139139
"siblings-label": "{number-siblings} of {total-siblings} symbols inside {parent-siblings}",
140140
"parent-label": "{number-siblings} of {total-siblings} symbols inside {parent-siblings} containing one symbol | {number-siblings} of {total-siblings} symbols inside {parent-siblings} containing {number-parent} symbols",
141-
"reset-filter": "Reset Filter"
141+
"reset-filter": "Reset Filter",
142+
"tags": {
143+
"sample-code": "Sample Code",
144+
"tutorials": "Tutorials",
145+
"articles": "Articles",
146+
"web-service-endpoints": "Web Service Endpoints",
147+
"hide-deprecated": "Hide Deprecated"
148+
}
142149
},
143150
"navigator": {
144151
"title": "Documentation Navigator",
@@ -152,9 +159,6 @@
152159
"state": {
153160
"loading": "loading",
154161
"ready": "ready"
155-
},
156-
"tags": {
157-
"hide-deprecated": "Hide Deprecated"
158162
}
159163
},
160164
"tab": {
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
/**
2+
* This source file is part of the Swift.org open source project
3+
*
4+
* Copyright (c) 2023-2024 Apple Inc. and the Swift project authors
5+
* Licensed under Apache License v2.0 with Runtime Library Exception
6+
*
7+
* See https://swift.org/LICENSE.txt for license information
8+
* See https://swift.org/CONTRIBUTORS.txt for Swift project authors
9+
*/
10+
11+
import { TOPIC_TYPE_TO_TAG, FILTER_TAGS } from 'docc-render/constants/Tags';
12+
13+
export default {
14+
computed: {
15+
/**
16+
* Returns a list of the child nodes, that match the filter pattern.
17+
* @returns NavigatorFlatItem[]
18+
*/
19+
filteredChildren: ({
20+
children,
21+
selectedTags,
22+
apiChanges,
23+
filterPattern,
24+
filterChildren,
25+
}) => (filterChildren(children, selectedTags, apiChanges, filterPattern)),
26+
},
27+
methods: {
28+
filterChildren(children, selectedTags, apiChanges, filterPattern) {
29+
// use Set over Array for better performance
30+
const selectedTagSet = new Set(selectedTags);
31+
// find children that match current filters
32+
return children.filter(({
33+
title, path, type, deprecated, deprecatedChildrenCount, childUIDs,
34+
}) => {
35+
// check if `title` matches the pattern, if provided
36+
const titleMatch = filterPattern ? filterPattern.test(title) : true;
37+
// groupMarkers know how many children they have and how many are deprecated
38+
const isDeprecated = deprecated || deprecatedChildrenCount === childUIDs.length;
39+
let tagMatch = true;
40+
if (selectedTagSet.size) {
41+
tagMatch = selectedTagSet.has(TOPIC_TYPE_TO_TAG[type]);
42+
// if there are API changes and there is no tag match, try to match change types
43+
if (apiChanges && !tagMatch) {
44+
const change = apiChanges[path];
45+
tagMatch = selectedTagSet.has(FILTER_TAGS[change]);
46+
}
47+
if (selectedTagSet.has(FILTER_TAGS.hideDeprecated)) {
48+
tagMatch = !isDeprecated;
49+
}
50+
}
51+
// find items, that have API changes
52+
const hasAPIChanges = apiChanges ? !!apiChanges[path] : true;
53+
// make sure groupMarker's don't get matched
54+
return titleMatch && tagMatch && hasAPIChanges;
55+
});
56+
},
57+
},
58+
};

0 commit comments

Comments
 (0)