Skip to content

Commit 8e9fdc6

Browse files
robert-lindstrompatrikhellgren
authored andcommitted
Add modern taxonomy picker
1 parent 8242c7a commit 8e9fdc6

File tree

8 files changed

+881
-0
lines changed

8 files changed

+881
-0
lines changed

src/ModernTaxonomyPicker.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export * from './controls/modernTaxonomyPicker';
Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
.modernTaxonomyPicker {
2+
3+
.contextualMenu {
4+
display: inline-block
5+
}
6+
7+
.listItem {
8+
min-height: 36px;
9+
line-height: 36px;
10+
cursor: pointer;
11+
12+
>div {
13+
display: inline-block;
14+
margin-right: 10px;
15+
}
16+
17+
img {
18+
margin-right: 5px;
19+
vertical-align: middle;
20+
}
21+
}
22+
23+
.termField {
24+
align-items: center;
25+
border-spacing: 0;
26+
display: flex;
27+
width: 100%;
28+
29+
.termFieldInput {
30+
flex-grow: 1;
31+
}
32+
33+
.termFieldButton {
34+
text-align: center;
35+
width: 42px;
36+
}
37+
38+
input[type="text"] {
39+
cursor: pointer;
40+
opacity: 0.8;
41+
width: 100%;
42+
}
43+
}
44+
45+
.termset {
46+
cursor: pointer;
47+
margin-left: 15px;
48+
}
49+
50+
.termSetSelectable {
51+
height: 50px;
52+
line-height: 50px;
53+
}
54+
55+
.termSetSelector {
56+
display: inline-block;
57+
margin: 0 8px 0 4px;
58+
vertical-align: middle;
59+
}
60+
61+
.term {
62+
padding-left: 20px;
63+
64+
.termEnabled,
65+
.termDisabled,
66+
.termNoTagging {
67+
background-repeat: no-repeat;
68+
background-position: 30px center;
69+
}
70+
71+
.termEnabled {
72+
background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAACzSURBVDhPY2AYNKCoqIgTiOcD8X8S8F6wB4Aa1IH4akNDw+mPHz++/E8EuHTp0jmQRSDNCcXFxa/XrVt3gAh9KEpgBvx/9OjRLVI1g9TDDYBp3rlz5//Kysr/IJoYgGEASPPatWsbQDQxAMOAbdu2gZ0FookBcAOePHlyhxgN6GqQY+Hdhg0bDpJqCNgAaDrQAnJuNDY2nvr06dMbYgw6e/bsabgBUEN4yEiJ2wdNViLfIQC3sTh2vtJcswAAAABJRU5ErkJggg=='); // /_layouts/15/Images/EMMTerm.png
73+
}
74+
75+
.termDisabled {
76+
background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAFMSURBVDhPzZPNSsNAFIULQqEQEPoSQqAg7gqC0HdwXxAElyYgBBGSZ+gDdOUyIauULATdKrgIKCKuQjbiDySCkNV4TrkzpJW2cefAxwwzc86de2em0/k3zXGcHpgC9Qeu5glAsAMefN+/K8vyVbVoWZbdMxDFY9d136Ioum6hW9iiDVSe588rxDXmJ+AAdAWOOVcbAy1O01R5nqfYoxVglyk+Hu7Z4FiwOcc1GBRMwQSnOAxDHz0jDyCwwCVQS3DO0gU0BkmSzG8A/UQiz7DxC5yLGQ1PwDeYGYOiKF6WarCPDUOJeor+A4z0m8P4SNaG+hY+4zi+aZh0scEBNeB41DTBuCcGjj6FjaM/BUFwW1XVO6vdMNiSdIzJLwN5TJZ+iSLQKYwbR9cmZyaFdX+JhZIiMue+cLFQxA0G22uusd/6I8OEb4LXRwZN4Q+3Ys8Mb9+nRgAAAABJRU5ErkJggg=='); // /_layouts/15/Images/EMMTermDeprecated.png
77+
}
78+
79+
.termNoTagging {
80+
background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAZBJREFUeNqkU0trwkAQnhURBBFPKghekuJN7KW9pOCj0Agl9/4uT/6BHgXpxQfxYKltKEYhV3tXFF/4RrOdXYgkxpz8YA47O983O9+whFIKt8DvddHv9wej0UjY7/dwPB55sGaJRKImSVLxXMiSl9Hr9fRWq0WvoVKpUFVVv6xaF1nX9R+LvFqteKAgrVarVNM0fi6Xy9Sq99mfjeTv2Wz2mMvlYL1en/OGYYCiKKTT6fAzG8flQbfb1ReLxX0+n4fJZOLwQxRFKJVKNJPJwHa7dZuI5I/lcsnJrHMwGHQUpdNpHp5bGI/Hr7IsA3Pc7/dcDEynU5jP5xAIBJwCpmmeN+IF9IaT0VA20p+V5yaGQqF2s9mE3W4Hh8PBFXZyLBbTstmsaAkQqyuu7nOz2UiCIEAkEoFwOMzz6A0n44YgHo8bhULBYQaxP7vRaLyjyFsqleIiDLbOAyTfXY5GLueu1+s1XNULE2Fg5Gg0qj4jrnlDrhmHIu3hcPh0Op0gmUz+IvfBy1xy62/0wY34F2AAKtctO7g/KgIAAAAASUVORK5CYII='); // /_layouts/15/Images/EMMTermDisabled.png
81+
}
82+
83+
label>span {
84+
padding-left: 25px;
85+
}
86+
}
87+
88+
.actions {
89+
button:first-child {
90+
margin-right: 15px;
91+
}
92+
}
93+
94+
.termBasePicker
95+
{
96+
background-color: #fff;
97+
}
98+
.termSuggestion
99+
{
100+
min-height: 40px;
101+
width: 100%;
102+
text-align: left;
103+
cursor: pointer;
104+
105+
106+
.termSuggestionSubTitle
107+
{
108+
font-size: 12px;
109+
color: #666666;
110+
}
111+
112+
}
113+
114+
.pickedTermRoot
115+
{
116+
position: relative;
117+
outline: transparent;
118+
box-sizing: content-box;
119+
flex-shrink: 1;
120+
background: #f4f4f4;
121+
margin: 2px;
122+
height: 26px;
123+
line-height: 26px;
124+
cursor: default;
125+
display: flex;
126+
flex-wrap: nowrap;
127+
max-width: 300px;
128+
129+
.pickedTermText
130+
{
131+
overflow: hidden;
132+
text-overflow: ellipsis;
133+
white-space: nowrap;
134+
min-width: 30px;
135+
margin: 0 8px;
136+
}
137+
.pickedTermCloseIcon
138+
{
139+
cursor: pointer;
140+
color: #666666;
141+
font-size: 12px;
142+
display: inline-block;
143+
text-align: center;
144+
vertical-align: top;
145+
width: 30px;
146+
height: 100%;
147+
-ms-flex-negative: 0;
148+
flex-shrink: 0;
149+
}
150+
}
151+
152+
.errorMessage {
153+
font-size: 12px;
154+
font-weight: 400;
155+
color: #a80000;
156+
margin: 0;
157+
padding-top: 5px;
158+
display: flex;
159+
align-items: center;
160+
}
161+
162+
.errorIcon {
163+
font-size: 14px;
164+
margin-right: 5px;
165+
}
166+
167+
}
Lines changed: 187 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,187 @@
1+
import * as React from 'react';
2+
import { BaseComponentContext } from '@microsoft/sp-component-base';
3+
import { Guid } from '@microsoft/sp-core-library';
4+
import { IIconProps } from 'office-ui-fabric-react/lib/components/Icon';
5+
import { PrimaryButton, DefaultButton, IconButton } from 'office-ui-fabric-react/lib/Button';
6+
import { Label } from 'office-ui-fabric-react/lib/Label';
7+
import { Panel, PanelType } from 'office-ui-fabric-react/lib/Panel';
8+
import { ITag, TagPicker } from 'office-ui-fabric-react/lib/Pickers';
9+
import { Spinner, SpinnerSize } from 'office-ui-fabric-react/lib/Spinner';
10+
import { IStackTokens, Stack } from 'office-ui-fabric-react/lib/Stack';
11+
import { sp } from '@pnp/sp';
12+
import { ITermInfo } from '@pnp/sp/taxonomy';
13+
import { SPTaxonomyService } from '../../services/SPTaxonomyService';
14+
import FieldErrorMessage from '../errorMessage/ErrorMessage';
15+
import { TaxonomyForm } from './taxonomyForm';
16+
import styles from './ModernTaxonomyPicker.module.scss';
17+
import * as strings from 'ControlStrings';
18+
19+
// TODO: remove/replace interface IPickerTerm
20+
export interface IPickerTerm {
21+
name: string;
22+
key: string;
23+
path: string;
24+
termSet: string;
25+
termSetName?: string;
26+
}
27+
28+
// TODO: remove/replace interface IPickerTerms
29+
export interface IPickerTerms extends Array<IPickerTerm> { }
30+
31+
export interface IModernTaxonomyPickerProps {
32+
allowMultipleSelections: boolean;
33+
termSetId: string;
34+
anchorTermId?: string;
35+
panelTitle: string;
36+
label: string;
37+
context: BaseComponentContext;
38+
initialValues?: ITag[];
39+
errorMessage?: string; // TODO: is this needed?
40+
disabled?: boolean;
41+
required?: boolean;
42+
}
43+
44+
export function ModernTaxonomyPicker(props: IModernTaxonomyPickerProps) {
45+
const [termsService] = React.useState(() => new SPTaxonomyService(props.context));
46+
const [terms, setTerms] = React.useState<ITermInfo[]>([]);
47+
const [errorMessage, setErrorMessage] = React.useState(props.errorMessage);
48+
const [internalErrorMessage, setInternalErrorMessage] = React.useState<string>();
49+
const [panelIsOpen, setPanelIsOpen] = React.useState(false);
50+
const [loading, setLoading] = React.useState(false); // was called loaded
51+
const [selectedOptions, setSelectedOptions] = React.useState<ITag[]>([]);
52+
const [selectedPanelOptions, setSelectedPanelOptions] = React.useState<ITag[]>([]);
53+
54+
const invalidTerm = React.useRef<string>(null);
55+
56+
React.useEffect(() => {
57+
sp.setup(props.context);
58+
}, []);
59+
60+
React.useEffect(() => {
61+
setSelectedOptions(props.initialValues || []);
62+
}, [props.initialValues]);
63+
64+
React.useEffect(() => {
65+
setErrorMessage(props.errorMessage);
66+
}, [props.errorMessage]);
67+
68+
async function onOpenPanel(): Promise<void> {
69+
if (props.disabled === true) {
70+
return;
71+
}
72+
setLoading(true);
73+
const siteUrl = props.context.pageContext.site.absoluteUrl;
74+
const newTerms = await termsService.getTerms(Guid.parse(props.termSetId), Guid.empty, '', true, 50);
75+
setTerms(newTerms.value);
76+
setLoading(false);
77+
setPanelIsOpen(true);
78+
}
79+
80+
function onClosePanel(): void {
81+
setLoading(false);
82+
setPanelIsOpen(false);
83+
}
84+
85+
function onSave(): void {
86+
setSelectedOptions([...selectedPanelOptions]);
87+
onClosePanel();
88+
}
89+
90+
async function onResolveSuggestions(filter: string, selectedItems?: ITag[]): Promise<ITag[]> {
91+
const languageTag = props.context.pageContext.cultureInfo.currentUICultureName !== '' ? props.context.pageContext.cultureInfo.currentUICultureName : props.context.pageContext.web.languageName;
92+
if (filter === '') {
93+
return [];
94+
}
95+
const filteredTerms = await termsService.searchTerm(Guid.parse(props.termSetId), filter, languageTag, props.anchorTermId ? Guid.parse(props.anchorTermId) : undefined);
96+
const filteredTermsWithoutSelectedItems = filteredTerms.filter((term) => {
97+
if (!selectedItems || selectedItems.length === 0) {
98+
return true;
99+
}
100+
for (const selectedItem of selectedItems) {
101+
return selectedItem.key !== term.id;
102+
}
103+
});
104+
const filteredTermsAndAvailable = filteredTermsWithoutSelectedItems.filter((term) => term.isAvailableForTagging.filter((t) => t.setId === props.termSetId)[0].isAvailable);
105+
const filteredTags = filteredTermsAndAvailable.map((term) => {
106+
const key = term.id;
107+
const name = term.labels.filter((termLabel) => (languageTag === '' || termLabel.languageTag === languageTag) &&
108+
termLabel.name.toLowerCase().indexOf(filter.toLowerCase()) === 0)[0]?.name;
109+
return { key: key, name: name };
110+
});
111+
return filteredTags;
112+
}
113+
114+
const { label, disabled, allowMultipleSelections, panelTitle, required } = props;
115+
return (
116+
<div className={styles.modernTaxonomyPicker}>
117+
{label && <Label required={required}>{label}</Label>}
118+
<div className={styles.termField}>
119+
<div className={styles.termFieldInput}>
120+
<TagPicker
121+
removeButtonAriaLabel="Remove"
122+
onResolveSuggestions={onResolveSuggestions}
123+
itemLimit={allowMultipleSelections ? undefined : 1}
124+
selectedItems={selectedOptions}
125+
onChange={(itms?: ITag[]) => {
126+
setSelectedOptions(itms || []);
127+
setSelectedPanelOptions(itms || []);
128+
}}
129+
getTextFromItem={(tag: ITag, currentValue?: string) => tag.name}
130+
inputProps={{
131+
'aria-label': 'Tag Picker',
132+
placeholder: 'Ange en term som du vill tagga'
133+
}}
134+
/>
135+
</div>
136+
<div className={styles.termFieldButton}>
137+
<IconButton disabled={disabled} iconProps={{ iconName: 'Tag' } as IIconProps} onClick={onOpenPanel} />
138+
</div>
139+
</div>
140+
141+
<FieldErrorMessage errorMessage={errorMessage || internalErrorMessage} />
142+
143+
<Panel
144+
isOpen={panelIsOpen}
145+
hasCloseButton={true}
146+
onDismiss={onClosePanel}
147+
isLightDismiss={true}
148+
type={PanelType.medium}
149+
headerText={panelTitle}
150+
onRenderFooterContent={() => {
151+
const horizontalGapStackTokens: IStackTokens = {
152+
childrenGap: 10,
153+
};
154+
return (
155+
<Stack horizontal disableShrink tokens={horizontalGapStackTokens}>
156+
<PrimaryButton text={strings.SaveButtonLabel} value="Save" onClick={onSave} />
157+
<DefaultButton text={strings.CancelButtonLabel} value="Cancel" onClick={onClosePanel} />
158+
</Stack>
159+
);
160+
}}>
161+
162+
{
163+
/* Show spinner in the panel while retrieving terms */
164+
loading === true ? <Spinner size={SpinnerSize.medium} /> : ''
165+
}
166+
{
167+
loading === false && props.termSetId && (
168+
<div key={props.termSetId} >
169+
<TaxonomyForm
170+
allowMultipleSelections={allowMultipleSelections}
171+
terms={terms}
172+
onResolveSuggestions={onResolveSuggestions}
173+
onLoadMoreData={termsService.getTerms}
174+
getTermSetInfo={termsService.getTermSetInfo}
175+
context={props.context}
176+
termSetId={Guid.parse(props.termSetId)}
177+
pageSize={50}
178+
selectedPanelOptions={selectedPanelOptions}
179+
setSelectedPanelOptions={setSelectedPanelOptions}
180+
/>
181+
</div>
182+
)
183+
}
184+
</Panel>
185+
</div >
186+
);
187+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export * from './ModernTaxonomyPicker';

0 commit comments

Comments
 (0)