├── .babelrc
├── .eslintrc.json
├── .flowconfig
├── .github
├── dependabot.yml
└── workflows
│ ├── actionlint.yml
│ └── ci.yml
├── .gitignore
├── .sass-lint.yml
├── CHANGELOG.md
├── LICENSE
├── README.md
├── dist
├── examples.css
├── index.esm.js
├── index.umd.js
└── miller-columns.css
├── examples.scss
├── examples
├── checkboxes-checked.html
├── index.html
└── miller-columns-test.html
├── index.js
├── miller-columns-selected.scss
├── miller-columns.scss
├── package-lock.json
├── package.json
├── prettier.config.js
└── test
├── .eslintrc.json
├── karma.config.js
└── test.js
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "env": {
3 | "esm": {
4 | "plugins": [
5 | "transform-custom-element-classes"
6 | ],
7 | "presets": [
8 | ["es2015", {"modules": false}],
9 | "flow"
10 | ]
11 | },
12 | "umd": {
13 | "plugins": [
14 | "transform-custom-element-classes",
15 | "transform-es2015-modules-umd"
16 | ],
17 | "presets": ["es2015", "flow"]
18 | }
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": [
3 | "plugin:github/es6",
4 | "plugin:github/browser"
5 | ],
6 | "parser": "babel-eslint"
7 | }
8 |
--------------------------------------------------------------------------------
/.flowconfig:
--------------------------------------------------------------------------------
1 | [ignore]
2 |
3 | [include]
4 |
5 | [libs]
6 |
7 | [options]
8 |
9 | [lints]
10 |
--------------------------------------------------------------------------------
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | version: 2
2 | updates:
3 | - package-ecosystem: npm
4 | directory: "/"
5 | dependency-type: production
6 | schedule:
7 | interval: daily
8 | time: "03:00"
9 | open-pull-requests-limit: 10
10 | ignore:
11 | - dependency-name: eslint-plugin-github
12 | versions:
13 | - "> 1.6.0"
14 | - dependency-name: flow-bin
15 | versions:
16 | - "> 0.133.0, < 1"
17 | - dependency-name: flow-bin
18 | versions:
19 | - ">= 0.145.a, < 0.146"
20 | - dependency-name: y18n
21 | versions:
22 | - 4.0.1
23 | - 4.0.2
24 | - 4.0.3
25 | - dependency-name: eslint
26 | versions:
27 | - 7.19.0
28 | - 7.21.0
29 | - 7.23.0
30 | - dependency-name: karma
31 | versions:
32 | - 6.0.3
33 | - 6.0.4
34 | - 6.1.0
35 | - 6.1.2
36 | - 6.3.0
37 | - 6.3.1
38 | - dependency-name: mocha
39 | versions:
40 | - 8.3.1
41 | - dependency-name: chai
42 | versions:
43 | - 4.3.1
44 | - 4.3.3
45 |
--------------------------------------------------------------------------------
/.github/workflows/actionlint.yml:
--------------------------------------------------------------------------------
1 | name: Lint GitHub Actions
2 | on:
3 | push:
4 | paths: ['.github/**']
5 | jobs:
6 | actionlint:
7 | runs-on: ubuntu-latest
8 | steps:
9 | - uses: actions/checkout@v4
10 | with:
11 | show-progress: false
12 | - uses: alphagov/govuk-infrastructure/.github/actions/actionlint@main
13 |
--------------------------------------------------------------------------------
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
1 | on: [push, pull_request]
2 | jobs:
3 | codeql-sast:
4 | name: CodeQL SAST scan
5 | uses: alphagov/govuk-infrastructure/.github/workflows/codeql-analysis.yml@main
6 | permissions:
7 | security-events: write
8 |
9 | dependency-review:
10 | name: Dependency Review scan
11 | uses: alphagov/govuk-infrastructure/.github/workflows/dependency-review.yml@main
12 |
13 | test:
14 | runs-on: ubuntu-latest
15 | steps:
16 | - uses: actions/checkout@v4
17 | - uses: actions/setup-node@v4
18 | with:
19 | node-version: 20
20 | cache: npm
21 | - run: npm ci
22 | - run: npm run test
23 | - run: npm run build
24 |
25 | release:
26 | needs: test
27 | runs-on: ubuntu-latest
28 | if: ${{ github.ref == 'refs/heads/main' }}
29 | permissions:
30 | contents: write
31 | steps:
32 | - uses: actions/checkout@v4
33 | - uses: actions/setup-node@v4
34 | with:
35 | node-version: 20
36 | cache: npm
37 | registry-url: 'https://registry.npmjs.org'
38 | - run: npm ci
39 | - run: npm run build
40 | - name: Deploy to GitHub Pages
41 | uses: JamesIves/github-pages-deploy-action@0f24da7de3e7e135102609a4c9633b025be8411b
42 | with:
43 | branch: gh-pages
44 | folder: examples
45 | - name: Establish version
46 | run: |
47 | LOCAL=$(node -p "require('./package.json').version")
48 | echo "local=${LOCAL}" >> "$GITHUB_OUTPUT"
49 | echo "remote=$(npm view miller-columns-element version)" >> "$GITHUB_OUTPUT"
50 | if git ls-remote --tags --exit-code origin "${LOCAL}"; then
51 | echo "tagged=yes" >> "$GITHUB_OUTPUT"
52 | fi
53 | id: version
54 | - name: Tag version
55 | if: ${{ steps.version.outputs.tagged != 'yes' }}
56 | run: git tag ${{ steps.version.outputs.local }} && git push --tags
57 | - name: Release to NPM
58 | if: ${{ steps.version.outputs.local != steps.version.outputs.remote }}
59 | run: npm publish
60 | env:
61 | NODE_AUTH_TOKEN: ${{ secrets.ALPHAGOV_NPM_AUTOMATION_TOKEN }}
62 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | examples/dist
3 |
--------------------------------------------------------------------------------
/.sass-lint.yml:
--------------------------------------------------------------------------------
1 | # Not yet supported by sass-lint:
2 | # ChainedClasses, DisableLinterReason, ElsePlacement, PropertyCount
3 | # PseudoElement, SelectorDepth, SpaceAroundOperator, TrailingWhitespace
4 | # UnnecessaryParentReference, Compass::*
5 | #
6 | # The following settings/values are unsupported by sass-lint:
7 | # Linter Comment, option "style"
8 | # Linter Indentation, option "allow_non_nested_indentation"
9 | # Linter Indentation, option "character"
10 | # Linter NestingDepth, option "ignore_parent_selectors"
11 | # Linter SpaceBeforeBrace, option "allow_single_line_padding"
12 |
13 | files:
14 | include: '**/*.s+(a|c)ss'
15 | options:
16 | formatter: stylish
17 | merge-default-rules: true
18 | rules:
19 |
20 | # Rule bem-depth will enforce how many elements a BEM selector can contain.
21 | # https://github.com/sasstools/sass-lint/blob/master/docs/rules/bem-depth.md
22 | bem-depth: 0
23 |
24 | # Rule border-zero will enforce whether one should use 0 or none when specifying a zero border value
25 | # https://github.com/sasstools/sass-lint/blob/master/docs/rules/border-zero.md
26 | border-zero: 2
27 |
28 | # Rule brace-style will enforce the use of the chosen brace style.
29 | # https://github.com/sasstools/sass-lint/blob/master/docs/rules/brace-style.md
30 | brace-style:
31 | - 2
32 | - allow-single-line: false
33 |
34 | # Rule class-name-format will enforce a convention for class names.
35 | # https://github.com/sasstools/sass-lint/blob/master/docs/rules/class-name-format.md#example-7
36 | # will allow .block {}, .block__element{}, .block--modifier {}
37 | class-name-format:
38 | - 2
39 | - convention: hyphenatedbem
40 |
41 | # Rule clean-import-paths will enforce whether or not @import paths should have leading underscores and/or filename extensions.
42 | # https://github.com/sasstools/sass-lint/blob/master/docs/rules/clean-import-paths.md
43 | clean-import-paths:
44 | - 2
45 | - filename-extension: false
46 | leading-underscore: false
47 |
48 | # TODO: empty-args
49 | # https://github.com/sasstools/sass-lint/blob/master/docs/rules/empty-args.md
50 |
51 | # Rule empty-line-between-blocks will enforce whether or not nested blocks should include a space between the last non-comment declaration or not.
52 | # https://github.com/sasstools/sass-lint/blob/master/docs/rules/empty-line-between-blocks.md
53 | empty-line-between-blocks:
54 | - 2
55 | - ignore-single-line-rulesets: true
56 |
57 | # Rule extends-before-declarations will enforce that extends should be written before declarations in a ruleset.
58 | # https://github.com/sasstools/sass-lint/blob/master/docs/rules/extends-before-declarations.md
59 | extends-before-declarations: 0
60 |
61 | # Rule extends-before-mixins will enforce that extends should be written before mixins in a ruleset.
62 | # https://github.com/sasstools/sass-lint/blob/master/docs/rules/extends-before-mixins.md
63 | extends-before-mixins: 0
64 |
65 | # Rule final-newline will enforce whether or not files should end with a newline.
66 | # https://github.com/sasstools/sass-lint/blob/master/docs/rules/final-newline.md
67 | final-newline:
68 | - 2
69 | - include: true
70 |
71 | # Rule force-attribute-nesting will enforce the nesting of attributes
72 | # https://github.com/sasstools/sass-lint/blob/master/docs/rules/force-attribute-nesting.md
73 | force-attribute-nesting:
74 | - 0
75 |
76 | # Rule force-element-nesting will enforce the nesting of elements
77 | # https://github.com/sasstools/sass-lint/blob/master/docs/rules/force-element-nesting.md
78 | force-element-nesting:
79 | - 0
80 |
81 | # Rule force-pseudo-nesting will enforce the nesting of pseudo elements/classes.
82 | # https://github.com/sasstools/sass-lint/blob/master/docs/rules/force-pseudo-nesting.md
83 | force-pseudo-nesting:
84 | - 0
85 |
86 | # Rule function-name-format will enforce a convention for function names.
87 | # https://github.com/sasstools/sass-lint/blob/master/docs/rules/function-name-format.md
88 | function-name-format:
89 | - 2
90 | - allow-leading-underscore: true
91 | convention: hyphenatedlowercase
92 |
93 | # Rule hex-length will enforce the length of hexadecimal values
94 | # https://github.com/sasstools/sass-lint/blob/master/docs/rules/hex-length.md
95 | hex-length:
96 | - 2
97 | - style: long
98 |
99 | # Rule hex-notation will enforce the case of hexadecimal values
100 | # https://github.com/sasstools/sass-lint/blob/master/docs/rules/hex-notation.md
101 | hex-notation:
102 | - 2
103 | - style: lowercase
104 |
105 | # Rule id-name-format will enforce a convention for ids.
106 | # https://github.com/sasstools/sass-lint/blob/master/docs/rules/id-name-format.md
107 | id-name-format:
108 | - 2
109 | - convention: hyphenatedlowercase
110 |
111 | # Rule indentation will enforce an indentation size (tabs and spaces) and it will also ensure that tabs and spaces are not mixed.
112 | # https://github.com/sasstools/sass-lint/blob/master/docs/rules/indentation.md
113 | indentation:
114 | - 2
115 | - size: 2
116 |
117 | # Rule leading-zero will enforce whether or not decimal numbers should include a leading zero.
118 | # https://github.com/sasstools/sass-lint/blob/master/docs/rules/leading-zero.md
119 | leading-zero:
120 | - 2
121 | - include: false
122 |
123 | # Rule mixin-name-format will enforce a convention for mixin names.
124 | # https://github.com/sasstools/sass-lint/blob/master/docs/rules/mixin-name-format.md
125 | mixin-name-format:
126 | - 2
127 | - allow-leading-underscore: true
128 | convention: hyphenatedlowercase
129 |
130 | # Rule mixins-before-declarations will enforce that mixins should be written before declarations in a ruleset.
131 | # https://github.com/sasstools/sass-lint/blob/master/docs/rules/mixins-before-declarations.md
132 | mixins-before-declarations: 0
133 |
134 | # Rule nesting-depth will enforce how deeply a selector can be nested.
135 | # https://github.com/sasstools/sass-lint/blob/master/docs/rules/nesting-depth.md
136 | nesting-depth:
137 | - 2
138 | - max-depth: 3
139 |
140 | # TODO: no-attribute-selectors
141 | # Rule no-attribute-selectors will warn against the use of attribute selectors.
142 | # https://github.com/sasstools/sass-lint/blob/master/docs/rules/no-attribute-selectors.md
143 |
144 | # TODO: no-colour-hex
145 | # https://github.com/sasstools/sass-lint/blob/master/docs/rules/no-color-hex.md
146 |
147 | # Rule no-color-keywords will enforce the use of hexadecimal color values rather than literals.
148 | # https://github.com/sasstools/sass-lint/blob/master/docs/rules/no-color-keywords.md
149 | no-color-keywords: 2
150 |
151 | # Rule no-color-literals will disallow the use of color literals and basic color functions in any declarations other than variables or maps/lists.
152 | # https://github.com/sasstools/sass-lint/blob/master/docs/rules/no-color-literals.md
153 | no-color-literals:
154 | - 2
155 | - allow-rgba: true
156 |
157 | # TODO: no-combniators
158 | # https://github.com/sasstools/sass-lint/blob/master/docs/rules/no-combinators.md
159 |
160 | # Rule no-css-comments will enforce the use of Sass single-line comments and disallow CSS comments. Bang comments (/*! */, will be printed even in minified mode) are still allowed.
161 | # https://github.com/sasstools/sass-lint/blob/master/docs/rules/no-css-comments.md
162 | no-css-comments: 2
163 |
164 | # Rule no-debug will enforce that @debug statements are not allowed to be used.
165 | # https://github.com/sasstools/sass-lint/blob/master/docs/rules/no-debug.md
166 | no-debug: 2
167 |
168 | # TODO: no-disallowed-properties
169 | # Rule no-disallowed-properties will warn against the use of certain properties.
170 | # https://github.com/sasstools/sass-lint/blob/master/docs/rules/no-disallowed-properties.md
171 |
172 | # Rule no-duplicate-properties will enforce that duplicate properties are not allowed within the same block.
173 | # https://github.com/sasstools/sass-lint/blob/master/docs/rules/no-duplicate-properties.md
174 | no-duplicate-properties: 2
175 |
176 | # Rule no-empty-rulesets will enforce that rulesets are not empty.
177 | # https://github.com/sasstools/sass-lint/blob/master/docs/rules/no-empty-rulesets.md
178 | no-empty-rulesets: 2
179 |
180 | # Rule no-extends will enforce that @extend is not allowed to be used.
181 | # https://github.com/sasstools/sass-lint/blob/master/docs/rules/no-extends.md
182 | no-extends: 0
183 |
184 | # Rule no-ids will enforce that ID selectors are not allowed to be used.
185 | # https://github.com/sasstools/sass-lint/blob/master/docs/rules/no-ids.md
186 | no-ids: 2
187 |
188 | # Rule no-important will enforce that important declarations are not allowed to be used.
189 | # https://github.com/sasstools/sass-lint/blob/master/docs/rules/no-important.md
190 | no-important: 0
191 |
192 | # Rule no-invalid-hex will enforce that only valid of hexadecimal values are written.
193 | # https://github.com/sasstools/sass-lint/blob/master/docs/rules/no-invalid-hex.md
194 | no-invalid-hex: 2
195 |
196 | # Rule no-mergeable-selectors will enforce that selectors aren't repeated and that their properties are merged.
197 | # https://github.com/sasstools/sass-lint/blob/master/docs/rules/no-mergeable-selectors.md
198 | no-mergeable-selectors: 0
199 |
200 | # Rule no-misspelled-properties will enforce the correct spelling of CSS properties and prevent the use of unknown CSS properties.
201 | # https://github.com/sasstools/sass-lint/blob/master/docs/rules/no-misspelled-properties.md
202 | no-misspelled-properties: 2
203 |
204 | # Rule no-qualifying-elements will enforce that selectors are not allowed to have qualifying elements.
205 | # https://github.com/sasstools/sass-lint/blob/master/docs/rules/no-qualifying-elements.md
206 | no-qualifying-elements:
207 | - 2
208 | - allow-element-with-attribute: true
209 | allow-element-with-class: false
210 | allow-element-with-id: false
211 |
212 | # TODO: no-trailing-whitespace
213 | # Rule no-trailing-whitespace will enforce that trailing whitespace is not allowed.
214 | # https://github.com/sasstools/sass-lint/blob/master/docs/rules/no-trailing-whitespace.md
215 |
216 | # Rule no-trailing-zero will enforce that trailing zeros are not allowed.
217 | # https://github.com/sasstools/sass-lint/blob/master/docs/rules/no-trailing-zero.md
218 | no-trailing-zero: 2
219 |
220 | # Rule no-transition-all will enforce whether the keyword all can be used with the transition or transition-property property.
221 | # https://github.com/sasstools/sass-lint/blob/master/docs/rules/no-transition-all.md
222 | no-transition-all: 2
223 |
224 | # TODO: no-universal-selectors
225 | # Rule no-universal-selectors will warn against the use of * (universal) selectors.
226 | # https://github.com/sasstools/sass-lint/blob/master/docs/rules/no-universal-selectors.md
227 |
228 | # Rule no-url-protocols will enforce that protocols and domains are not used within urls.
229 | # https://github.com/sasstools/sass-lint/blob/master/docs/rules/no-url-protocols.md
230 | no-url-protocols: 2
231 |
232 | # Rule no-vendor-prefixes will enforce that vendor prefixes are not allowed to be used.
233 | # https://github.com/sasstools/sass-lint/blob/master/docs/rules/no-vendor-prefixes.md
234 | no-vendor-prefixes: 0
235 |
236 | # Rule no-warn will enforce that @warn statements are not allowed to be used.
237 | # https://github.com/sasstools/sass-lint/blob/master/docs/rules/no-warn.md
238 | no-warn: 0
239 |
240 | # Rule placeholder-in-extend will enforce whether extends should only include placeholder selectors.
241 | # https://github.com/sasstools/sass-lint/blob/master/docs/rules/placeholder-in-extend.md
242 | placeholder-in-extend: 2
243 |
244 | # Rule placeholder-name-format will enforce a convention for placeholder names.
245 | # https://github.com/sasstools/sass-lint/blob/master/docs/rules/placeholder-name-format.md
246 | placeholder-name-format:
247 | - 2
248 | - convention: hyphenatedlowercase
249 |
250 | # Rule property-sort-order will enforce the order in which declarations are written.
251 | # https://github.com/sasstools/sass-lint/blob/master/docs/rules/property-sort-order.md
252 | property-sort-order:
253 | - 2
254 | - order:
255 | - 'content'
256 | - 'quotes'
257 | # Box-sizing - Allow here until global is decided
258 | - 'box-sizing'
259 | -
260 | - 'display'
261 | - 'visibility'
262 | -
263 | - 'position'
264 | - 'z-index'
265 | - 'top'
266 | - 'right'
267 | - 'bottom'
268 | - 'left'
269 | -
270 | - 'width'
271 | - 'min-width'
272 | - 'max-width'
273 | - 'height'
274 | - 'min-height'
275 | - 'max-height'
276 | -
277 | - 'margin'
278 | - 'margin-top'
279 | - 'margin-right'
280 | - 'margin-bottom'
281 | - 'margin-left'
282 | -
283 | - 'padding'
284 | - 'padding-top'
285 | - 'padding-right'
286 | - 'padding-bottom'
287 | - 'padding-left'
288 | -
289 | - 'float'
290 | - 'clear'
291 | -
292 | - 'overflow'
293 | - 'overflow-x'
294 | - 'overflow-y'
295 | -
296 | - 'clip'
297 | - 'clip-path'
298 | - 'zoom'
299 | - 'resize'
300 | -
301 | - 'columns'
302 | -
303 | - 'table-layout'
304 | - 'empty-cells'
305 | - 'caption-side'
306 | - 'border-spacing'
307 | - 'border-collapse'
308 | -
309 | - 'list-style'
310 | - 'list-style-position'
311 | - 'list-style-type'
312 | - 'list-style-image'
313 | -
314 | - 'transform'
315 | - 'transition'
316 | - 'animation'
317 | -
318 | - 'border'
319 | - 'border-top'
320 | - 'border-right'
321 | - 'border-bottom'
322 | - 'border-left'
323 | -
324 | - 'border-width'
325 | - 'border-top-width'
326 | - 'border-right-width'
327 | - 'border-bottom-width'
328 | - 'border-left-width'
329 | -
330 | - 'border-style'
331 | - 'border-top-style'
332 | - 'border-right-style'
333 | - 'border-bottom-style'
334 | - 'border-left-style'
335 | -
336 | - 'border-radius'
337 | - 'border-top-left-radius'
338 | - 'border-top-right-radius'
339 | - 'border-bottom-left-radius'
340 | - 'border-bottom-right-radius'
341 | -
342 | - 'border-color'
343 | - 'border-top-color'
344 | - 'border-right-color'
345 | - 'border-bottom-color'
346 | - 'border-left-color'
347 | -
348 | - 'outline'
349 | - 'outline-color'
350 | - 'outline-offset'
351 | - 'outline-style'
352 | - 'outline-width'
353 | -
354 | - 'opacity'
355 | # Color has been moved to ensure it appears before background
356 | - 'color'
357 | - 'background'
358 | - 'background-color'
359 | - 'background-image'
360 | - 'background-repeat'
361 | - 'background-position'
362 | - 'background-size'
363 | - 'box-shadow'
364 | - 'fill'
365 | -
366 | - 'font'
367 | - 'font-family'
368 | - 'font-size'
369 | - 'font-style'
370 | - 'font-variant'
371 | - 'font-weight'
372 | -
373 | - 'font-emphasize'
374 | -
375 | - 'letter-spacing'
376 | - 'line-height'
377 | - 'list-style'
378 | - 'word-spacing'
379 | -
380 | - 'text-align'
381 | - 'text-align-last'
382 | - 'text-decoration'
383 | - 'text-indent'
384 | - 'text-justify'
385 | - 'text-overflow'
386 | - 'text-overflow-ellipsis'
387 | - 'text-overflow-mode'
388 | - 'text-rendering'
389 | - 'text-outline'
390 | - 'text-shadow'
391 | - 'text-transform'
392 | - 'text-wrap'
393 | - 'word-wrap'
394 | - 'word-break'
395 | -
396 | - 'text-emphasis'
397 | -
398 | - 'vertical-align'
399 | - 'white-space'
400 | - 'word-spacing'
401 | - 'hyphens'
402 | -
403 | - 'src'
404 | - 'cursor'
405 | - '-webkit-appearance'
406 |
407 | # Rule property-units will disallow the use of units not specified in global or per-property.
408 | # https://github.com/sasstools/sass-lint/blob/master/docs/rules/property-units.md
409 | property-units:
410 | - 2
411 | - global: [
412 | 'cm',
413 | 'em',
414 | 'pt',
415 | 'px',
416 | 'rem',
417 | 'vh',
418 | 'ex'
419 | ]
420 |
421 | # Rule pseudo-element will enforce that:
422 | # - pseudo-elements must start with double colons
423 | # - pseudo-classes must start with single colon
424 | # https://github.com/sasstools/sass-lint/blob/master/docs/rules/pseudo-element.md
425 | pseudo-element:
426 | - 0
427 |
428 | # Rule quotes will enforce whether single quotes ('') or double quotes ("") should be used for all strings.
429 | # https://github.com/sasstools/sass-lint/blob/master/docs/rules/quotes.md
430 | quotes:
431 | - 2
432 | - style: double
433 |
434 | # Rule shorthand-values will enforce that values in their shorthand form are as concise as specified.
435 | # https://github.com/sasstools/sass-lint/blob/master/docs/rules/shorthand-values.md
436 | shorthand-values:
437 | - 0
438 | - allowed-shorthands:
439 | - 1
440 | - 2
441 | - 3
442 |
443 | # Rule single-line-per-selector will enforce whether selectors should be placed on a new line.
444 | # https://github.com/sasstools/sass-lint/blob/master/docs/rules/single-line-per-selector.md
445 | single-line-per-selector: 2
446 |
447 | # Rule space-after-bang will enforce whether or not a space should be included after a bang (!).
448 | # https://github.com/sasstools/sass-lint/blob/master/docs/rules/space-after-bang.md
449 | space-after-bang:
450 | - 2
451 | - include: false
452 |
453 | # Rule space-after-colon will enforce whether or not a space should be included after a colon (:).
454 | # https://github.com/sasstools/sass-lint/blob/master/docs/rules/space-after-colon.md
455 | space-after-colon:
456 | - 2
457 | - include: true
458 |
459 | # Rule space-after-comma will enforce whether or not a space should be included after a comma (,).
460 | # https://github.com/sasstools/sass-lint/blob/master/docs/rules/space-after-comma.md
461 | space-after-comma:
462 | - 2
463 | - include: true
464 |
465 | # Rule space-around-operator will enforce whether or not a single space should be included before and after the following operators: +, -, /, *, %, <, > ==, !=, <= and >=.
466 | # https://github.com/sasstools/sass-lint/blob/master/docs/rules/space-around-operator.md
467 | space-around-operator:
468 | - 2
469 | - include: true
470 |
471 | # Rule space-before-bang will enforce whether or not a space should be included before a bang (!).
472 | # https://github.com/sasstools/sass-lint/blob/master/docs/rules/space-before-bang.md
473 | space-before-bang:
474 | - 2
475 | - include: true
476 |
477 | # Rule space-before-brace will enforce whether or not a space should be included before a brace ({).
478 | # https://github.com/sasstools/sass-lint/blob/master/docs/rules/space-before-brace.md
479 | space-before-brace:
480 | - 2
481 | - include: true
482 |
483 | # Rule space-before-colon will enforce whether or not a space should be included before a colon (:).
484 | # https://github.com/sasstools/sass-lint/blob/master/docs/rules/space-before-colon.md
485 | space-before-colon: 2
486 |
487 | # Rule space-between-parens will enforce whether or not a space should be included before the first item and after the last item inside parenthesis (()).
488 | # https://github.com/sasstools/sass-lint/blob/master/docs/rules/space-between-parens.md
489 | space-between-parens: 2
490 |
491 | # Rule trailing-semicolon will enforce whether the last declaration in a block should include a semicolon (;) or not.
492 | # https://github.com/sasstools/sass-lint/blob/master/docs/rules/trailing-semicolon.md
493 | trailing-semicolon: 2
494 |
495 | # Rule url-quotes will enforce that URLs are wrapped in quotes.
496 | # https://github.com/sasstools/sass-lint/blob/master/docs/rules/url-quotes.md
497 | url-quotes: 2
498 |
499 | # Rule variable-for-property will enforce the use of variables for the values of specified properties.
500 | # https://github.com/sasstools/sass-lint/blob/master/docs/rules/variable-for-property.md
501 | variable-for-property:
502 | - 0
503 | - properties: []
504 |
505 | # Rule variable-name-format will enforce a convention for variable names.
506 | # https://github.com/sasstools/sass-lint/blob/master/docs/rules/variable-name-format.md
507 | variable-name-format:
508 | - 2
509 | - allow-leading-underscore: true
510 | convention: hyphenatedlowercase
511 |
512 | # Rule zero-unit will enforce whether or not values of 0 used for length should be unitless.
513 | # https://github.com/sasstools/sass-lint/blob/master/docs/rules/zero-unit.md
514 | zero-unit: 2
515 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Changelog
2 |
3 | - We use [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
4 | - Mark breaking changes with `BREAKING:`. Be sure to include instructions on
5 | how applications should be upgraded.
6 | - Include a link to your pull request.
7 | - Don't include changes that are purely internal. The CHANGELOG should be a
8 | useful summary for people upgrading their application, not a replication
9 | of the commit log.
10 |
11 | ## 2.0.1
12 |
13 | - Add Github Action Linting and update Github Workflow to address deprecation errors
14 |
15 | ## 2.0.0
16 |
17 | - BREAKING: govuk-frontend classes are no longer bundled with this module, apps using this are expected to already be using `govuk-frontend` (PR #97)
18 |
19 | ## 1.3.2
20 |
21 | - Fix back-link styles by updating miller-columns-element to `govuk-frontend` 3.7 (PR #75)
22 |
23 | ## 1.3.1
24 |
25 | - Ensure checkboxes are focusable without JS (PR #35)
26 |
27 | ## 1.3.0
28 |
29 | - Fix active column on root taxons (PR #30)
30 | - Enable keyboard navigation (PR #31)
31 | - Enable navigation instructions for assistive technologies (PR #32)
32 |
33 | ## 1.2.1
34 |
35 | - Improve remove action for screen readers (PR #28)
36 |
37 | ## 1.2.0
38 |
39 | - Enhance mobile view (PR #25)
40 | - Update miller-columns-element to `govuk-frontend` 3.3 (PR #25)
41 |
42 | ## 1.1.0
43 |
44 | - Update miller-columns-element to `govuk-frontend` 3.2 (PR #23)
45 |
46 | ## 1.0.0
47 |
48 | - Trigger click event on checkboxes when selecting an item in the MillerColumnsElement (PR #16)
49 | - BREAKING: Remove `selectedTopicNames` as we don't need this API method for analytics anymore (PR #17)
50 |
51 | ## 0.1.1
52 |
53 | - Scope pointer events on checkboxes to the miller-columns element (PR #14)
54 | - Trigger remove-topic event on MillerColumnsSelectedElement (PR #15)
55 |
56 | ## 0.1.0
57 |
58 | - Initial release
59 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (C) 2019 Crown Copyright (Government Digital Service)
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy of
6 | this software and associated documentation files (the "Software"), to deal in
7 | the Software without restriction, including without limitation the rights to
8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
9 | of the Software, and to permit persons to whom the Software is furnished to do
10 | so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # <miller-columns> element
2 |
3 | Express a hierarchy by showing selectable lists of the items in each hierarchy level.
4 |
5 | Selection of any item shows that item’s children in the next list.
6 |
7 | ## Installation
8 |
9 | ```
10 | npm install --save miller-columns-element
11 | ```
12 |
13 | ## Usage
14 |
15 | This element is expected to be used in an application with [govuk-frontend](https://github.com/alphagov/govuk-frontend) installed. The expected CSS dependencies are outlined in [examples.scss](./examples.scss).
16 |
17 | ```html
18 |
19 | Use the right arrow to explore sub-topics, use the up and down arrows to find other topics.
20 |
21 |
22 |
23 |
24 |
25 |
47 |
48 | ```
49 |
50 | ## Browser support
51 |
52 | Browsers without native [custom element support][support] require a [polyfill][].
53 |
54 | - Chrome
55 | - Firefox
56 | - Safari
57 | - Internet Explorer 11
58 | - Microsoft Edge
59 |
60 | [support]: https://caniuse.com/#feat=custom-elementsv1
61 | [polyfill]: https://github.com/webcomponents/custom-elements
62 |
63 | ## Development
64 |
65 | ```
66 | npm install
67 | npm test
68 | ```
69 |
70 | To continuously build files while developing run:
71 |
72 | ```
73 | npm run watch
74 | ```
75 |
76 | To install and run a local HTTP server using Node.js:
77 |
78 | ```
79 | npm install -g http-server
80 | http-server
81 | ```
82 |
83 | To manually check examples in a web browser or using BrowserStack:
84 |
85 | - `http://127.0.0.1:8080/examples/index.html` (default example)
86 | - `http://127.0.0.1:8080/examples/checkboxes-checked.html` (with pre-selected items at page load)
87 | - `http://127.0.0.1:8080/examples/miller-columns-test.html` (example used for tests)
88 |
89 | Alternatively, you can open one of the HTML files in the [`/examples`](https://github.com/alphagov/miller-columns-element/tree/master/examples) directory for a quick preview.
90 |
91 | ## License
92 |
93 | Distributed under the MIT license. See LICENSE for details.
94 |
--------------------------------------------------------------------------------
/dist/index.esm.js:
--------------------------------------------------------------------------------
1 | var _slicedToArray = function () { function sliceIterator(arr, i) { var _arr = []; var _n = true; var _d = false; var _e = undefined; try { for (var _i = arr[Symbol.iterator](), _s; !(_n = (_s = _i.next()).done); _n = true) { _arr.push(_s.value); if (i && _arr.length === i) break; } } catch (err) { _d = true; _e = err; } finally { try { if (!_n && _i["return"]) _i["return"](); } finally { if (_d) throw _e; } } return _arr; } return function (arr, i) { if (Array.isArray(arr)) { return arr; } else if (Symbol.iterator in Object(arr)) { return sliceIterator(arr, i); } else { throw new TypeError("Invalid attempt to destructure non-iterable instance"); } }; }();
2 |
3 | var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }();
4 |
5 | function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; }
6 |
7 | function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; }
8 |
9 | function _CustomElement() {
10 | return Reflect.construct(HTMLElement, [], this.__proto__.constructor);
11 | }
12 |
13 | ;
14 | Object.setPrototypeOf(_CustomElement.prototype, HTMLElement.prototype);
15 | Object.setPrototypeOf(_CustomElement, HTMLElement);
16 |
17 | function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
18 |
19 | function nodesToArray(nodes) {
20 | return Array.prototype.slice.call(nodes);
21 | }
22 |
23 | function triggerEvent(element, eventName, detail) {
24 | var params = { bubbles: true, cancelable: true, detail: detail || null };
25 | var event = void 0;
26 |
27 | if (typeof window.CustomEvent === 'function') {
28 | event = new window.CustomEvent(eventName, params);
29 | } else {
30 | event = document.createEvent('CustomEvent');
31 | event.initCustomEvent(eventName, params.bubbles, params.cancelable, params.detail);
32 | }
33 |
34 | element.dispatchEvent(event);
35 | }
36 |
37 | /**
38 | * This models the taxonomy shown in the miller columns and the current state
39 | * of it.
40 | * It notifies the miller columns element when it has changed state to update
41 | * the UI
42 | */
43 |
44 | var Taxonomy = function () {
45 | function Taxonomy(topics, millerColumns) {
46 | _classCallCheck(this, Taxonomy);
47 |
48 | this.topics = topics;
49 | this.millerColumns = millerColumns;
50 | this.active = this.selectedTopics[0];
51 | }
52 |
53 | /** fetches all the topics that are currently selected */
54 |
55 | // At any time there is one or no active topic, the active topic determines
56 | // what part of the taxonomy is currently shown to the user (i.e which level)
57 | // if this is null a user is shown the root column
58 |
59 |
60 | _createClass(Taxonomy, [{
61 | key: 'topicClicked',
62 |
63 |
64 | /** Handler for a topic in the miller columns being clicked */
65 | value: function topicClicked(topic) {
66 | // if this is the active topic or a parent of it we deselect
67 | if (topic === this.active || topic.parentOf(this.active)) {
68 | topic.deselect(true);
69 | this.active = topic.parent;
70 | } else if (topic.selected || topic.selectedChildren.length) {
71 | // if this is a selected topic with children we make it active to allow
72 | // picking the children
73 | if (topic.children.length) {
74 | this.active = topic;
75 | } else {
76 | // otherwise we deselect it as we know the user can't be traversing
77 | topic.deselect(true);
78 | this.active = topic.parent;
79 | }
80 | } else {
81 | // otherwise this is a new selection
82 | topic.select();
83 | this.active = topic;
84 | }
85 | this.millerColumns.update();
86 | }
87 |
88 | /** Handler for when a topic is removed via the selected element */
89 |
90 | }, {
91 | key: 'removeTopic',
92 | value: function removeTopic(topic) {
93 | topic.deselect(false);
94 | // determine which topic to mark as active, if any
95 | this.active = this.determineActiveFromRemoved(topic);
96 | this.millerColumns.update();
97 | }
98 |
99 | /** Calculate most relevant topic to show user after they've removed a topic */
100 |
101 | }, {
102 | key: 'determineActiveFromRemoved',
103 | value: function determineActiveFromRemoved(topic) {
104 | // if there is already an active item with selected children lets not
105 | // change anything
106 | if (this.active && (this.active.selected || this.active.selectedChildren.length)) {
107 | return this.active;
108 | }
109 |
110 | // see if there is a parent with selected topics, that feels like the most
111 | // natural place to end up
112 | var _iteratorNormalCompletion = true;
113 | var _didIteratorError = false;
114 | var _iteratorError = undefined;
115 |
116 | try {
117 | for (var _iterator = topic.parents.reverse()[Symbol.iterator](), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true) {
118 | var parent = _step.value;
119 |
120 | if (parent.selectedChildren.length) {
121 | return parent;
122 | }
123 | }
124 |
125 | // if we've still not got one we'll go for the first selected one
126 | } catch (err) {
127 | _didIteratorError = true;
128 | _iteratorError = err;
129 | } finally {
130 | try {
131 | if (!_iteratorNormalCompletion && _iterator.return) {
132 | _iterator.return();
133 | }
134 | } finally {
135 | if (_didIteratorError) {
136 | throw _iteratorError;
137 | }
138 | }
139 | }
140 |
141 | return this.selectedTopics[0];
142 | }
143 | }, {
144 | key: 'selectedTopics',
145 | get: function get() {
146 | return this.topics.reduce(function (memo, topic) {
147 | if (topic.selected) {
148 | memo.push(topic);
149 | }
150 |
151 | return memo.concat(topic.selectedChildren);
152 | }, []);
153 | }
154 | }, {
155 | key: 'flattenedTopics',
156 | get: function get() {
157 | return this.topics.reduce(function (memo, topic) {
158 | memo.push(topic);
159 | return memo.concat(topic.flattenedChildren);
160 | }, []);
161 | }
162 | }]);
163 |
164 | return Taxonomy;
165 | }();
166 |
167 | /**
168 | * Represents a single topic in the taxonomy and knows whether it is currently
169 | * selected or not
170 | */
171 |
172 |
173 | var Topic = function () {
174 | _createClass(Topic, null, [{
175 | key: 'fromList',
176 | value: function fromList(list) {
177 | var parent = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : null;
178 |
179 | var topics = [];
180 | if (!list) {
181 | return topics;
182 | }
183 |
184 | var children = Array.from(list.children);
185 |
186 | var _iteratorNormalCompletion2 = true;
187 | var _didIteratorError2 = false;
188 | var _iteratorError2 = undefined;
189 |
190 | try {
191 | for (var _iterator2 = children.entries()[Symbol.iterator](), _step2; !(_iteratorNormalCompletion2 = (_step2 = _iterator2.next()).done); _iteratorNormalCompletion2 = true) {
192 | var _step2$value = _slicedToArray(_step2.value, 2),
193 | index = _step2$value[0],
194 | item = _step2$value[1];
195 |
196 | var label = item.querySelector('label');
197 | var checkbox = item.querySelector('input');
198 | if (label instanceof HTMLLabelElement && checkbox instanceof HTMLInputElement) {
199 | var childList = item.querySelector('ul');
200 | childList = childList instanceof HTMLUListElement ? childList : null;
201 |
202 | checkbox.tabIndex = -1;
203 |
204 | var previous = index > 0 ? topics[index - 1] : null;
205 |
206 | var topic = new Topic(label, checkbox, childList, parent, previous);
207 |
208 | if (index > 0) {
209 | topics[index - 1].next = topic;
210 | }
211 |
212 | topics.push(topic);
213 | }
214 | }
215 | } catch (err) {
216 | _didIteratorError2 = true;
217 | _iteratorError2 = err;
218 | } finally {
219 | try {
220 | if (!_iteratorNormalCompletion2 && _iterator2.return) {
221 | _iterator2.return();
222 | }
223 | } finally {
224 | if (_didIteratorError2) {
225 | throw _iteratorError2;
226 | }
227 | }
228 | }
229 |
230 | return topics;
231 | }
232 | // Whether this topic is selected, we only allow one item in a branch of the
233 | // taxonomy to be selected.
234 | // E.g. given education > school > 6th form only one of these can be selected
235 | // at a time and the parents are implicity selected from it
236 |
237 | }]);
238 |
239 | function Topic(label, checkbox, childList, parent, previous) {
240 | _classCallCheck(this, Topic);
241 |
242 | this.label = label;
243 | this.checkbox = checkbox;
244 | this.parent = parent;
245 | this.children = Topic.fromList(childList, this);
246 | this.previous = previous;
247 |
248 | if (this.checkbox.checked) {
249 | this.select();
250 | } else {
251 | this.selected = false;
252 | }
253 | }
254 |
255 | _createClass(Topic, [{
256 | key: 'parentOf',
257 |
258 |
259 | /** Whether this topic is the parent of a different one */
260 | value: function parentOf(other) {
261 | if (!other) {
262 | return false;
263 | }
264 |
265 | var _iteratorNormalCompletion3 = true;
266 | var _didIteratorError3 = false;
267 | var _iteratorError3 = undefined;
268 |
269 | try {
270 | for (var _iterator3 = this.children[Symbol.iterator](), _step3; !(_iteratorNormalCompletion3 = (_step3 = _iterator3.next()).done); _iteratorNormalCompletion3 = true) {
271 | var topic = _step3.value;
272 |
273 | if (topic === other || topic.parentOf(other)) {
274 | return true;
275 | }
276 | }
277 | } catch (err) {
278 | _didIteratorError3 = true;
279 | _iteratorError3 = err;
280 | } finally {
281 | try {
282 | if (!_iteratorNormalCompletion3 && _iterator3.return) {
283 | _iterator3.return();
284 | }
285 | } finally {
286 | if (_didIteratorError3) {
287 | throw _iteratorError3;
288 | }
289 | }
290 | }
291 |
292 | return false;
293 | }
294 | }, {
295 | key: 'withParents',
296 | value: function withParents() {
297 | return this.parents.concat([this]);
298 | }
299 |
300 | /** Attempts to select this topic assuming it's not alrerady selected or has selected children */
301 |
302 | }, {
303 | key: 'select',
304 | value: function select() {
305 | // if already selected or a child is selected do nothing
306 | if (this.selected || this.selectedChildren.length) {
307 | return;
308 | }
309 | this.selected = true;
310 | this.checkbox.checked = true;
311 | if (this.parent) {
312 | this.parent.childWasSelected();
313 | }
314 | }
315 |
316 | /**
317 | * Deselects this topic. If this item is not itself selected but a child of it
318 | * is then it traverses to that child and deselects it.
319 | * Takes an optional argument as to whether to select the parent after deselection
320 | * Doing this allows a user to stay in context of their selection in the miller
321 | * column element as deselecting the whole tree would take them back to root
322 | */
323 |
324 | }, {
325 | key: 'deselect',
326 | value: function deselect() {
327 | var selectParent = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : true;
328 |
329 | // if this item is selected explicitly we can deselect it
330 | if (this.selected) {
331 | this.deselectSelfAndParents();
332 | } else {
333 | // otherwise we need to find the selected children to start deselecting
334 | var selectedChildren = this.selectedChildren;
335 |
336 | // if we have none it's a no-op
337 | if (!selectedChildren.length) {
338 | return;
339 | }
340 |
341 | var _iteratorNormalCompletion4 = true;
342 | var _didIteratorError4 = false;
343 | var _iteratorError4 = undefined;
344 |
345 | try {
346 | for (var _iterator4 = selectedChildren[Symbol.iterator](), _step4; !(_iteratorNormalCompletion4 = (_step4 = _iterator4.next()).done); _iteratorNormalCompletion4 = true) {
347 | var child = _step4.value;
348 |
349 | child.deselect(false);
350 | }
351 | } catch (err) {
352 | _didIteratorError4 = true;
353 | _iteratorError4 = err;
354 | } finally {
355 | try {
356 | if (!_iteratorNormalCompletion4 && _iterator4.return) {
357 | _iterator4.return();
358 | }
359 | } finally {
360 | if (_didIteratorError4) {
361 | throw _iteratorError4;
362 | }
363 | }
364 | }
365 | }
366 |
367 | if (selectParent && this.parent) {
368 | this.parent.select();
369 | }
370 | }
371 | }, {
372 | key: 'deselectSelfAndParents',
373 | value: function deselectSelfAndParents() {
374 | // loop through the parents only deselecting items that don't have other
375 | // selected children
376 | var _iteratorNormalCompletion5 = true;
377 | var _didIteratorError5 = false;
378 | var _iteratorError5 = undefined;
379 |
380 | try {
381 | for (var _iterator5 = this.withParents().reverse()[Symbol.iterator](), _step5; !(_iteratorNormalCompletion5 = (_step5 = _iterator5.next()).done); _iteratorNormalCompletion5 = true) {
382 | var topic = _step5.value;
383 |
384 | if (topic.selectedChildren.length) {
385 | break;
386 | } else {
387 | topic.selected = false;
388 | topic.checkbox.checked = false;
389 | }
390 | }
391 | } catch (err) {
392 | _didIteratorError5 = true;
393 | _iteratorError5 = err;
394 | } finally {
395 | try {
396 | if (!_iteratorNormalCompletion5 && _iterator5.return) {
397 | _iterator5.return();
398 | }
399 | } finally {
400 | if (_didIteratorError5) {
401 | throw _iteratorError5;
402 | }
403 | }
404 | }
405 | }
406 |
407 | /** If a child is selected we need to implicitly select all the parents */
408 |
409 | }, {
410 | key: 'childWasSelected',
411 | value: function childWasSelected() {
412 | this.checkbox.checked = true;
413 | this.selected = false;
414 | if (this.parent) {
415 | this.parent.childWasSelected();
416 | }
417 | }
418 | }, {
419 | key: 'topicName',
420 | get: function get() {
421 | return this.label.textContent.replace(/(^\s+|\s+$)/g, '');
422 | }
423 | }, {
424 | key: 'topicNames',
425 | get: function get() {
426 | var items = [];
427 | var _iteratorNormalCompletion6 = true;
428 | var _didIteratorError6 = false;
429 | var _iteratorError6 = undefined;
430 |
431 | try {
432 | for (var _iterator6 = this.parents[Symbol.iterator](), _step6; !(_iteratorNormalCompletion6 = (_step6 = _iterator6.next()).done); _iteratorNormalCompletion6 = true) {
433 | var parent = _step6.value;
434 |
435 | items.push(parent.topicName);
436 | }
437 | } catch (err) {
438 | _didIteratorError6 = true;
439 | _iteratorError6 = err;
440 | } finally {
441 | try {
442 | if (!_iteratorNormalCompletion6 && _iterator6.return) {
443 | _iterator6.return();
444 | }
445 | } finally {
446 | if (_didIteratorError6) {
447 | throw _iteratorError6;
448 | }
449 | }
450 | }
451 |
452 | items.push(this.topicName);
453 | return items;
454 | }
455 |
456 | /** The presence of selected children determines whether this item is considered selected */
457 |
458 | }, {
459 | key: 'selectedChildren',
460 | get: function get() {
461 | return this.children.reduce(function (memo, topic) {
462 | var selected = topic.selectedChildren;
463 | if (topic.selected) {
464 | selected.push(topic);
465 | }
466 | return memo.concat(selected);
467 | }, []);
468 | }
469 | }, {
470 | key: 'parents',
471 | get: function get() {
472 | if (this.parent) {
473 | return this.parent.parents.concat([this.parent]);
474 | } else {
475 | return [];
476 | }
477 | }
478 | }, {
479 | key: 'flattenedChildren',
480 | get: function get() {
481 | return this.children.reduce(function (memo, topic) {
482 | memo.push(topic);
483 | return memo.concat(topic.flattenedChildren);
484 | }, []);
485 | }
486 | }]);
487 |
488 | return Topic;
489 | }();
490 |
491 | var MillerColumnsElement = function (_CustomElement2) {
492 | _inherits(MillerColumnsElement, _CustomElement2);
493 |
494 | function MillerColumnsElement() {
495 | _classCallCheck(this, MillerColumnsElement);
496 |
497 | var _this = _possibleConstructorReturn(this, (MillerColumnsElement.__proto__ || Object.getPrototypeOf(MillerColumnsElement)).call(this));
498 |
499 | _this.classNames = {
500 | column: 'miller-columns__column',
501 | columnHeading: 'miller-columns__column-heading',
502 | backLink: 'govuk-back-link',
503 | columnList: 'miller-columns__column-list',
504 | columnCollapse: 'miller-columns__column--collapse',
505 | columnMedium: 'miller-columns__column--medium',
506 | columnNarrow: 'miller-columns__column--narrow',
507 | columnActive: 'miller-columns__column--active',
508 | item: 'miller-columns__item',
509 | itemParent: 'miller-columns__item--parent',
510 | itemActive: 'miller-columns__item--active',
511 | itemSelected: 'miller-columns__item--selected'
512 | };
513 | return _this;
514 | }
515 |
516 | _createClass(MillerColumnsElement, [{
517 | key: 'connectedCallback',
518 | value: function connectedCallback() {
519 | this.describedbyId = this.getAttribute('aria-describedby');
520 |
521 | var source = document.getElementById(this.getAttribute('for') || '');
522 | if (source) {
523 | this.taxonomy = new Taxonomy(Topic.fromList(source), this);
524 | this.renderTaxonomyColumn(this.taxonomy.topics, true);
525 | this.update();
526 | if (source.parentNode) {
527 | source.parentNode.removeChild(source);
528 | }
529 | this.style.display = 'block';
530 | }
531 | }
532 |
533 | /** Returns the element which shows the selections a user has made */
534 |
535 | }, {
536 | key: 'renderTaxonomyColumn',
537 |
538 |
539 | /** Build and insert a column of the taxonomy */
540 | value: function renderTaxonomyColumn(topics) {
541 | var _this2 = this;
542 |
543 | var root = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : false;
544 |
545 | var div = document.createElement('div');
546 |
547 | if (!root) {
548 | // Append back link
549 | var backLink = document.createElement('button');
550 | backLink.className = this.classNames.backLink;
551 | backLink.type = 'button';
552 | backLink.innerHTML = 'Back';
553 | backLink.addEventListener('click', function () {
554 | if (topics[0].parent) {
555 | _this2.showCurrentColumns(topics[0].parent.parent);
556 | }
557 | }, false);
558 | div.appendChild(backLink);
559 |
560 | // Append heading
561 | var h3 = document.createElement('h3');
562 | h3.className = this.classNames.columnHeading;
563 | var parentTopicName = topics[0].parent ? topics[0].parent.topicName : null;
564 | if (parentTopicName) {
565 | h3.innerHTML = parentTopicName;
566 | }
567 | div.appendChild(h3);
568 | }
569 |
570 | // Append list
571 | var ul = document.createElement('ul');
572 | ul.className = this.classNames.columnList;
573 | div.className = this.classNames.column;
574 | if (root) {
575 | div.dataset.root = 'true';
576 | } else {
577 | div.classList.add(this.classNames.columnCollapse);
578 | }
579 | div.appendChild(ul);
580 |
581 | // Append column
582 | this.appendChild(div);
583 |
584 | var _iteratorNormalCompletion7 = true;
585 | var _didIteratorError7 = false;
586 | var _iteratorError7 = undefined;
587 |
588 | try {
589 | for (var _iterator7 = topics[Symbol.iterator](), _step7; !(_iteratorNormalCompletion7 = (_step7 = _iterator7.next()).done); _iteratorNormalCompletion7 = true) {
590 | var topic = _step7.value;
591 |
592 | this.renderTopic(topic, ul);
593 | }
594 | } catch (err) {
595 | _didIteratorError7 = true;
596 | _iteratorError7 = err;
597 | } finally {
598 | try {
599 | if (!_iteratorNormalCompletion7 && _iterator7.return) {
600 | _iterator7.return();
601 | }
602 | } finally {
603 | if (_didIteratorError7) {
604 | throw _iteratorError7;
605 | }
606 | }
607 | }
608 | }
609 |
610 | /** Build and insert a list item for a topic */
611 |
612 | }, {
613 | key: 'renderTopic',
614 | value: function renderTopic(topic, list) {
615 | var li = document.createElement('li');
616 | li.classList.add(this.classNames.item);
617 | li.classList.add('govuk-checkboxes--small');
618 | if (this.describedbyId) {
619 | li.setAttribute('aria-describedby', this.describedbyId);
620 | }
621 |
622 | var div = document.createElement('div');
623 | div.className = 'govuk-checkboxes__item';
624 | div.appendChild(topic.checkbox);
625 | div.appendChild(topic.label);
626 | li.appendChild(div);
627 | list.appendChild(li);
628 | this.attachEvents(li, topic);
629 |
630 | if (topic.children.length) {
631 | li.classList.add(this.classNames.itemParent);
632 | this.renderTaxonomyColumn(topic.children);
633 | }
634 | }
635 |
636 | /** Focus the miller columns item associated with a topic */
637 |
638 | }, {
639 | key: 'focusTopic',
640 | value: function focusTopic(topic) {
641 | if (topic instanceof Topic && topic.checkbox) {
642 | var item = topic.checkbox.closest('.' + this.classNames.item);
643 | if (item instanceof HTMLElement) {
644 | item.focus();
645 | }
646 | }
647 | }
648 |
649 | /** Sets up the event handling for a list item and a topic */
650 |
651 | }, {
652 | key: 'attachEvents',
653 | value: function attachEvents(trigger, topic) {
654 | var _this3 = this;
655 |
656 | trigger.tabIndex = 0;
657 | trigger.addEventListener('click', function () {
658 | _this3.taxonomy.topicClicked(topic);
659 | topic.checkbox.dispatchEvent(new Event('click'));
660 | }, false);
661 | trigger.addEventListener('keydown', function (event) {
662 | switch (event.key) {
663 | case ' ':
664 | case 'Enter':
665 | event.preventDefault();
666 | _this3.taxonomy.topicClicked(topic);
667 | topic.checkbox.dispatchEvent(new Event('click'));
668 | break;
669 | case 'ArrowUp':
670 | event.preventDefault();
671 | if (topic.previous) {
672 | _this3.showCurrentColumns(topic.previous);
673 | _this3.focusTopic(topic.previous);
674 | }
675 | break;
676 | case 'ArrowDown':
677 | event.preventDefault();
678 | if (topic.next) {
679 | _this3.showCurrentColumns(topic.next);
680 | _this3.focusTopic(topic.next);
681 | }
682 | break;
683 | case 'ArrowLeft':
684 | event.preventDefault();
685 | if (topic.parent) {
686 | _this3.showCurrentColumns(topic.parent);
687 | _this3.focusTopic(topic.parent);
688 | }
689 | break;
690 | case 'ArrowRight':
691 | event.preventDefault();
692 | if (topic.children) {
693 | _this3.showCurrentColumns(topic.children[0]);
694 | _this3.focusTopic(topic.children[0]);
695 | }
696 | break;
697 | default:
698 | return;
699 | }
700 | }, false);
701 | }
702 |
703 | /** Update this element to show a change in the state */
704 |
705 | }, {
706 | key: 'update',
707 | value: function update() {
708 | this.showSelectedTopics(this.taxonomy.selectedTopics);
709 | this.showActiveTopic(this.taxonomy.active);
710 | this.showCurrentColumns(this.taxonomy.active);
711 |
712 | if (this.selectedElement) {
713 | this.selectedElement.update(this.taxonomy);
714 | }
715 | }
716 |
717 | /**
718 | * Utility method to swap class names over for a group of elements
719 | * Takes an array of all elements that should have a class and removes it
720 | * from any other items that have it
721 | */
722 |
723 | }, {
724 | key: 'updateClassName',
725 | value: function updateClassName(className, items) {
726 | var currentlyWithClass = nodesToArray(this.getElementsByClassName(className));
727 |
728 | var _iteratorNormalCompletion8 = true;
729 | var _didIteratorError8 = false;
730 | var _iteratorError8 = undefined;
731 |
732 | try {
733 | for (var _iterator8 = currentlyWithClass.concat(items)[Symbol.iterator](), _step8; !(_iteratorNormalCompletion8 = (_step8 = _iterator8.next()).done); _iteratorNormalCompletion8 = true) {
734 | var item = _step8.value;
735 |
736 | if (!item) {
737 | continue;
738 | }
739 |
740 | if (items.indexOf(item) !== -1) {
741 | item.classList.add(className);
742 | } else {
743 | item.classList.remove(className);
744 | }
745 | }
746 | } catch (err) {
747 | _didIteratorError8 = true;
748 | _iteratorError8 = err;
749 | } finally {
750 | try {
751 | if (!_iteratorNormalCompletion8 && _iterator8.return) {
752 | _iterator8.return();
753 | }
754 | } finally {
755 | if (_didIteratorError8) {
756 | throw _iteratorError8;
757 | }
758 | }
759 | }
760 | }
761 |
762 | /** Given an array of selected topics update the UI */
763 |
764 | }, {
765 | key: 'showSelectedTopics',
766 | value: function showSelectedTopics(selectedTopics) {
767 | var _this4 = this;
768 |
769 | var selectedItems = selectedTopics.reduce(function (memo, child) {
770 | var _iteratorNormalCompletion9 = true;
771 | var _didIteratorError9 = false;
772 | var _iteratorError9 = undefined;
773 |
774 | try {
775 | for (var _iterator9 = child.withParents()[Symbol.iterator](), _step9; !(_iteratorNormalCompletion9 = (_step9 = _iterator9.next()).done); _iteratorNormalCompletion9 = true) {
776 | var topic = _step9.value;
777 |
778 | var item = topic.checkbox.closest('.' + _this4.classNames.item);
779 | if (item instanceof HTMLElement) {
780 | memo.push(item);
781 | }
782 | }
783 | } catch (err) {
784 | _didIteratorError9 = true;
785 | _iteratorError9 = err;
786 | } finally {
787 | try {
788 | if (!_iteratorNormalCompletion9 && _iterator9.return) {
789 | _iterator9.return();
790 | }
791 | } finally {
792 | if (_didIteratorError9) {
793 | throw _iteratorError9;
794 | }
795 | }
796 | }
797 |
798 | return memo;
799 | }, []);
800 |
801 | this.updateClassName(this.classNames.itemSelected, selectedItems);
802 | }
803 |
804 | /** Update the topic items for the presence (or not) of an active topic */
805 |
806 | }, {
807 | key: 'showActiveTopic',
808 | value: function showActiveTopic(activeTopic) {
809 | var _this5 = this;
810 |
811 | var activeItems = void 0;
812 |
813 | if (!activeTopic) {
814 | activeItems = [];
815 | } else {
816 | activeItems = activeTopic.withParents().reduce(function (memo, topic) {
817 | var item = topic.checkbox.closest('.' + _this5.classNames.item);
818 |
819 | if (item instanceof HTMLElement) {
820 | memo.push(item);
821 | }
822 |
823 | return memo;
824 | }, []);
825 | }
826 | this.updateClassName(this.classNames.itemActive, activeItems);
827 | }
828 |
829 | /** Change what columns are visible based on the active (or not) topic */
830 |
831 | }, {
832 | key: 'showCurrentColumns',
833 | value: function showCurrentColumns(activeTopic) {
834 | var allColumns = nodesToArray(this.getElementsByClassName(this.classNames.column));
835 | var columnsToShow = activeTopic ? this.columnsForActiveTopic(activeTopic) : [allColumns[0]];
836 | var narrowThreshold = Math.max(3, columnsToShow.length - 1);
837 | var showNarrow = columnsToShow.length > narrowThreshold;
838 | var showMedium = showNarrow && narrowThreshold === 3;
839 | var _classNames = this.classNames,
840 | collapseClass = _classNames.columnCollapse,
841 | narrowClass = _classNames.columnNarrow,
842 | mediumClass = _classNames.columnMedium,
843 | activeClass = _classNames.columnActive;
844 | var _iteratorNormalCompletion10 = true;
845 | var _didIteratorError10 = false;
846 | var _iteratorError10 = undefined;
847 |
848 | try {
849 |
850 | for (var _iterator10 = allColumns[Symbol.iterator](), _step10; !(_iteratorNormalCompletion10 = (_step10 = _iterator10.next()).done); _iteratorNormalCompletion10 = true) {
851 | var item = _step10.value;
852 |
853 | if (!item) {
854 | continue;
855 | }
856 |
857 | item.classList.remove(activeClass);
858 | // we always want to show the root column
859 | if (item.dataset.root === 'true') {
860 | item.classList.remove(narrowClass, mediumClass);
861 | if (showMedium) {
862 | item.classList.add(mediumClass);
863 | } else if (showNarrow) {
864 | item.classList.add(narrowClass);
865 | }
866 | if (columnsToShow.length === 1) {
867 | item.classList.add(activeClass);
868 | }
869 | continue;
870 | }
871 |
872 | var index = columnsToShow.indexOf(item);
873 |
874 | if (index === -1) {
875 | // this is not a column to show
876 | item.classList.add(collapseClass);
877 | } else if (showNarrow && index < narrowThreshold) {
878 | // show this column but narrow
879 | item.classList.remove(collapseClass, narrowClass, mediumClass);
880 | if (showMedium) {
881 | item.classList.add(mediumClass);
882 | } else if (showNarrow) {
883 | item.classList.add(narrowClass);
884 | }
885 | } else {
886 | // show this column in all it's glory
887 | item.classList.remove(collapseClass, narrowClass, mediumClass);
888 | }
889 |
890 | // mark last visible column as active
891 | if (item === columnsToShow[columnsToShow.length - 1]) {
892 | item.classList.add(activeClass);
893 | }
894 | }
895 | } catch (err) {
896 | _didIteratorError10 = true;
897 | _iteratorError10 = err;
898 | } finally {
899 | try {
900 | if (!_iteratorNormalCompletion10 && _iterator10.return) {
901 | _iterator10.return();
902 | }
903 | } finally {
904 | if (_didIteratorError10) {
905 | throw _iteratorError10;
906 | }
907 | }
908 | }
909 | }
910 |
911 | /** Determine which columns should be shown based on the active topic */
912 |
913 | }, {
914 | key: 'columnsForActiveTopic',
915 | value: function columnsForActiveTopic(activeTopic) {
916 | if (!activeTopic) {
917 | return [];
918 | }
919 |
920 | var columnSelector = '.' + this.classNames.column;
921 | var columns = activeTopic.withParents().reduce(function (memo, topic) {
922 | var column = topic.checkbox.closest(columnSelector);
923 | if (column instanceof HTMLElement) {
924 | memo.push(column);
925 | }
926 |
927 | return memo;
928 | }, []);
929 |
930 | // we'll want to show the next column too for the next choices
931 | if (activeTopic.children.length) {
932 | var nextColumn = activeTopic.children[0].checkbox.closest(columnSelector);
933 | if (nextColumn instanceof HTMLElement) {
934 | columns.push(nextColumn);
935 | }
936 | }
937 | return columns;
938 | }
939 | }, {
940 | key: 'selectedElement',
941 | get: function get() {
942 | var selected = document.getElementById(this.getAttribute('selected') || '');
943 | return selected instanceof MillerColumnsSelectedElement ? selected : null;
944 | }
945 | }]);
946 |
947 | return MillerColumnsElement;
948 | }(_CustomElement);
949 |
950 | var MillerColumnsSelectedElement = function (_CustomElement3) {
951 | _inherits(MillerColumnsSelectedElement, _CustomElement3);
952 |
953 | function MillerColumnsSelectedElement() {
954 | _classCallCheck(this, MillerColumnsSelectedElement);
955 |
956 | return _possibleConstructorReturn(this, (MillerColumnsSelectedElement.__proto__ || Object.getPrototypeOf(MillerColumnsSelectedElement)).call(this));
957 | }
958 |
959 | _createClass(MillerColumnsSelectedElement, [{
960 | key: 'connectedCallback',
961 | value: function connectedCallback() {
962 | this.list = document.createElement('ol');
963 | this.list.className = 'miller-columns-selected__list';
964 | this.appendChild(this.list);
965 | if (this.millerColumnsElement && this.millerColumnsElement.taxonomy) {
966 | this.update(this.millerColumnsElement.taxonomy);
967 | }
968 | }
969 | }, {
970 | key: 'update',
971 |
972 |
973 | /** Update the UI to show the selected topics */
974 | value: function update(taxonomy) {
975 | this.taxonomy = taxonomy;
976 | var selectedTopics = taxonomy.selectedTopics;
977 | // seems simpler to nuke the list and re-build it
978 | while (this.list.lastChild) {
979 | this.list.removeChild(this.list.lastChild);
980 | }
981 |
982 | if (selectedTopics.length) {
983 | var _iteratorNormalCompletion11 = true;
984 | var _didIteratorError11 = false;
985 | var _iteratorError11 = undefined;
986 |
987 | try {
988 | for (var _iterator11 = selectedTopics[Symbol.iterator](), _step11; !(_iteratorNormalCompletion11 = (_step11 = _iterator11.next()).done); _iteratorNormalCompletion11 = true) {
989 | var topic = _step11.value;
990 |
991 | this.addSelectedTopic(topic);
992 | }
993 | } catch (err) {
994 | _didIteratorError11 = true;
995 | _iteratorError11 = err;
996 | } finally {
997 | try {
998 | if (!_iteratorNormalCompletion11 && _iterator11.return) {
999 | _iterator11.return();
1000 | }
1001 | } finally {
1002 | if (_didIteratorError11) {
1003 | throw _iteratorError11;
1004 | }
1005 | }
1006 | }
1007 | } else {
1008 | var li = document.createElement('li');
1009 | li.className = 'miller-columns-selected__list-item';
1010 | li.textContent = 'No selected topics';
1011 | this.list.appendChild(li);
1012 | }
1013 | }
1014 | }, {
1015 | key: 'addSelectedTopic',
1016 | value: function addSelectedTopic(topic) {
1017 | var li = document.createElement('li');
1018 | li.className = 'miller-columns-selected__list-item';
1019 | li.appendChild(this.breadcrumbsElement(topic));
1020 | li.appendChild(this.removeTopicElement(topic));
1021 | this.list.appendChild(li);
1022 | }
1023 | }, {
1024 | key: 'breadcrumbsElement',
1025 | value: function breadcrumbsElement(topic) {
1026 | var div = document.createElement('div');
1027 | div.className = 'govuk-breadcrumbs';
1028 | var ol = document.createElement('ol');
1029 | ol.className = 'govuk-breadcrumbs__list';
1030 | var _iteratorNormalCompletion12 = true;
1031 | var _didIteratorError12 = false;
1032 | var _iteratorError12 = undefined;
1033 |
1034 | try {
1035 | for (var _iterator12 = topic.withParents()[Symbol.iterator](), _step12; !(_iteratorNormalCompletion12 = (_step12 = _iterator12.next()).done); _iteratorNormalCompletion12 = true) {
1036 | var current = _step12.value;
1037 |
1038 | var li = document.createElement('li');
1039 | li.className = 'govuk-breadcrumbs__list-item';
1040 | li.textContent = current.label.textContent;
1041 | ol.appendChild(li);
1042 | }
1043 | } catch (err) {
1044 | _didIteratorError12 = true;
1045 | _iteratorError12 = err;
1046 | } finally {
1047 | try {
1048 | if (!_iteratorNormalCompletion12 && _iterator12.return) {
1049 | _iterator12.return();
1050 | }
1051 | } finally {
1052 | if (_didIteratorError12) {
1053 | throw _iteratorError12;
1054 | }
1055 | }
1056 | }
1057 |
1058 | div.appendChild(ol);
1059 | return div;
1060 | }
1061 | }, {
1062 | key: 'removeTopicElement',
1063 | value: function removeTopicElement(topic) {
1064 | var _this7 = this;
1065 |
1066 | var button = document.createElement('button');
1067 | button.className = 'miller-columns-selected__remove-topic';
1068 | button.textContent = 'Remove topic';
1069 | button.setAttribute('type', 'button');
1070 | button.addEventListener('click', function () {
1071 | triggerEvent(button, 'remove-topic', topic);
1072 | if (_this7.taxonomy) {
1073 | _this7.taxonomy.removeTopic(topic);
1074 | }
1075 | });
1076 |
1077 | var span = document.createElement('span');
1078 | span.className = 'miller-columns-selected__remove-topic-name';
1079 | span.textContent = ': ' + topic.topicName;
1080 | button.appendChild(span);
1081 |
1082 | return button;
1083 | }
1084 | }, {
1085 | key: 'millerColumnsElement',
1086 | get: function get() {
1087 | var millerColumns = document.getElementById(this.getAttribute('for') || '');
1088 | return millerColumns instanceof MillerColumnsElement ? millerColumns : null;
1089 | }
1090 | }]);
1091 |
1092 | return MillerColumnsSelectedElement;
1093 | }(_CustomElement);
1094 |
1095 | if (!window.customElements.get('miller-columns')) {
1096 | window.MillerColumnsElement = MillerColumnsElement;
1097 | window.customElements.define('miller-columns', MillerColumnsElement);
1098 | }
1099 |
1100 | if (!window.customElements.get('miller-columns-selected')) {
1101 | window.MillerColumnsSelectedElement = MillerColumnsSelectedElement;
1102 | window.customElements.define('miller-columns-selected', MillerColumnsSelectedElement);
1103 | }
1104 |
1105 | export { MillerColumnsElement, MillerColumnsSelectedElement };
1106 |
--------------------------------------------------------------------------------
/dist/index.umd.js:
--------------------------------------------------------------------------------
1 | (function (global, factory) {
2 | if (typeof define === "function" && define.amd) {
3 | define(['exports'], factory);
4 | } else if (typeof exports !== "undefined") {
5 | factory(exports);
6 | } else {
7 | var mod = {
8 | exports: {}
9 | };
10 | factory(mod.exports);
11 | global.index = mod.exports;
12 | }
13 | })(this, function (exports) {
14 | 'use strict';
15 |
16 | Object.defineProperty(exports, "__esModule", {
17 | value: true
18 | });
19 |
20 | function _possibleConstructorReturn(self, call) {
21 | if (!self) {
22 | throw new ReferenceError("this hasn't been initialised - super() hasn't been called");
23 | }
24 |
25 | return call && (typeof call === "object" || typeof call === "function") ? call : self;
26 | }
27 |
28 | function _inherits(subClass, superClass) {
29 | if (typeof superClass !== "function" && superClass !== null) {
30 | throw new TypeError("Super expression must either be null or a function, not " + typeof superClass);
31 | }
32 |
33 | subClass.prototype = Object.create(superClass && superClass.prototype, {
34 | constructor: {
35 | value: subClass,
36 | enumerable: false,
37 | writable: true,
38 | configurable: true
39 | }
40 | });
41 | if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass;
42 | }
43 |
44 | function _CustomElement() {
45 | return Reflect.construct(HTMLElement, [], this.__proto__.constructor);
46 | }
47 |
48 | ;
49 | Object.setPrototypeOf(_CustomElement.prototype, HTMLElement.prototype);
50 | Object.setPrototypeOf(_CustomElement, HTMLElement);
51 |
52 | var _slicedToArray = function () {
53 | function sliceIterator(arr, i) {
54 | var _arr = [];
55 | var _n = true;
56 | var _d = false;
57 | var _e = undefined;
58 |
59 | try {
60 | for (var _i = arr[Symbol.iterator](), _s; !(_n = (_s = _i.next()).done); _n = true) {
61 | _arr.push(_s.value);
62 |
63 | if (i && _arr.length === i) break;
64 | }
65 | } catch (err) {
66 | _d = true;
67 | _e = err;
68 | } finally {
69 | try {
70 | if (!_n && _i["return"]) _i["return"]();
71 | } finally {
72 | if (_d) throw _e;
73 | }
74 | }
75 |
76 | return _arr;
77 | }
78 |
79 | return function (arr, i) {
80 | if (Array.isArray(arr)) {
81 | return arr;
82 | } else if (Symbol.iterator in Object(arr)) {
83 | return sliceIterator(arr, i);
84 | } else {
85 | throw new TypeError("Invalid attempt to destructure non-iterable instance");
86 | }
87 | };
88 | }();
89 |
90 | function _classCallCheck(instance, Constructor) {
91 | if (!(instance instanceof Constructor)) {
92 | throw new TypeError("Cannot call a class as a function");
93 | }
94 | }
95 |
96 | var _createClass = function () {
97 | function defineProperties(target, props) {
98 | for (var i = 0; i < props.length; i++) {
99 | var descriptor = props[i];
100 | descriptor.enumerable = descriptor.enumerable || false;
101 | descriptor.configurable = true;
102 | if ("value" in descriptor) descriptor.writable = true;
103 | Object.defineProperty(target, descriptor.key, descriptor);
104 | }
105 | }
106 |
107 | return function (Constructor, protoProps, staticProps) {
108 | if (protoProps) defineProperties(Constructor.prototype, protoProps);
109 | if (staticProps) defineProperties(Constructor, staticProps);
110 | return Constructor;
111 | };
112 | }();
113 |
114 | function nodesToArray(nodes) {
115 | return Array.prototype.slice.call(nodes);
116 | }
117 |
118 | function triggerEvent(element, eventName, detail) {
119 | var params = { bubbles: true, cancelable: true, detail: detail || null };
120 | var event = void 0;
121 |
122 | if (typeof window.CustomEvent === 'function') {
123 | event = new window.CustomEvent(eventName, params);
124 | } else {
125 | event = document.createEvent('CustomEvent');
126 | event.initCustomEvent(eventName, params.bubbles, params.cancelable, params.detail);
127 | }
128 |
129 | element.dispatchEvent(event);
130 | }
131 |
132 | /**
133 | * This models the taxonomy shown in the miller columns and the current state
134 | * of it.
135 | * It notifies the miller columns element when it has changed state to update
136 | * the UI
137 | */
138 |
139 | var Taxonomy = function () {
140 | function Taxonomy(topics, millerColumns) {
141 | _classCallCheck(this, Taxonomy);
142 |
143 | this.topics = topics;
144 | this.millerColumns = millerColumns;
145 | this.active = this.selectedTopics[0];
146 | }
147 |
148 | /** fetches all the topics that are currently selected */
149 |
150 | // At any time there is one or no active topic, the active topic determines
151 | // what part of the taxonomy is currently shown to the user (i.e which level)
152 | // if this is null a user is shown the root column
153 |
154 |
155 | _createClass(Taxonomy, [{
156 | key: 'topicClicked',
157 | value: function topicClicked(topic) {
158 | // if this is the active topic or a parent of it we deselect
159 | if (topic === this.active || topic.parentOf(this.active)) {
160 | topic.deselect(true);
161 | this.active = topic.parent;
162 | } else if (topic.selected || topic.selectedChildren.length) {
163 | // if this is a selected topic with children we make it active to allow
164 | // picking the children
165 | if (topic.children.length) {
166 | this.active = topic;
167 | } else {
168 | // otherwise we deselect it as we know the user can't be traversing
169 | topic.deselect(true);
170 | this.active = topic.parent;
171 | }
172 | } else {
173 | // otherwise this is a new selection
174 | topic.select();
175 | this.active = topic;
176 | }
177 | this.millerColumns.update();
178 | }
179 | }, {
180 | key: 'removeTopic',
181 | value: function removeTopic(topic) {
182 | topic.deselect(false);
183 | // determine which topic to mark as active, if any
184 | this.active = this.determineActiveFromRemoved(topic);
185 | this.millerColumns.update();
186 | }
187 | }, {
188 | key: 'determineActiveFromRemoved',
189 | value: function determineActiveFromRemoved(topic) {
190 | // if there is already an active item with selected children lets not
191 | // change anything
192 | if (this.active && (this.active.selected || this.active.selectedChildren.length)) {
193 | return this.active;
194 | }
195 |
196 | // see if there is a parent with selected topics, that feels like the most
197 | // natural place to end up
198 | var _iteratorNormalCompletion = true;
199 | var _didIteratorError = false;
200 | var _iteratorError = undefined;
201 |
202 | try {
203 | for (var _iterator = topic.parents.reverse()[Symbol.iterator](), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true) {
204 | var parent = _step.value;
205 |
206 | if (parent.selectedChildren.length) {
207 | return parent;
208 | }
209 | }
210 |
211 | // if we've still not got one we'll go for the first selected one
212 | } catch (err) {
213 | _didIteratorError = true;
214 | _iteratorError = err;
215 | } finally {
216 | try {
217 | if (!_iteratorNormalCompletion && _iterator.return) {
218 | _iterator.return();
219 | }
220 | } finally {
221 | if (_didIteratorError) {
222 | throw _iteratorError;
223 | }
224 | }
225 | }
226 |
227 | return this.selectedTopics[0];
228 | }
229 | }, {
230 | key: 'selectedTopics',
231 | get: function get() {
232 | return this.topics.reduce(function (memo, topic) {
233 | if (topic.selected) {
234 | memo.push(topic);
235 | }
236 |
237 | return memo.concat(topic.selectedChildren);
238 | }, []);
239 | }
240 | }, {
241 | key: 'flattenedTopics',
242 | get: function get() {
243 | return this.topics.reduce(function (memo, topic) {
244 | memo.push(topic);
245 | return memo.concat(topic.flattenedChildren);
246 | }, []);
247 | }
248 | }]);
249 |
250 | return Taxonomy;
251 | }();
252 |
253 | var Topic = function () {
254 | _createClass(Topic, null, [{
255 | key: 'fromList',
256 | value: function fromList(list) {
257 | var parent = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : null;
258 |
259 | var topics = [];
260 | if (!list) {
261 | return topics;
262 | }
263 |
264 | var children = Array.from(list.children);
265 |
266 | var _iteratorNormalCompletion2 = true;
267 | var _didIteratorError2 = false;
268 | var _iteratorError2 = undefined;
269 |
270 | try {
271 | for (var _iterator2 = children.entries()[Symbol.iterator](), _step2; !(_iteratorNormalCompletion2 = (_step2 = _iterator2.next()).done); _iteratorNormalCompletion2 = true) {
272 | var _step2$value = _slicedToArray(_step2.value, 2),
273 | index = _step2$value[0],
274 | item = _step2$value[1];
275 |
276 | var label = item.querySelector('label');
277 | var checkbox = item.querySelector('input');
278 | if (label instanceof HTMLLabelElement && checkbox instanceof HTMLInputElement) {
279 | var childList = item.querySelector('ul');
280 | childList = childList instanceof HTMLUListElement ? childList : null;
281 |
282 | checkbox.tabIndex = -1;
283 |
284 | var previous = index > 0 ? topics[index - 1] : null;
285 |
286 | var topic = new Topic(label, checkbox, childList, parent, previous);
287 |
288 | if (index > 0) {
289 | topics[index - 1].next = topic;
290 | }
291 |
292 | topics.push(topic);
293 | }
294 | }
295 | } catch (err) {
296 | _didIteratorError2 = true;
297 | _iteratorError2 = err;
298 | } finally {
299 | try {
300 | if (!_iteratorNormalCompletion2 && _iterator2.return) {
301 | _iterator2.return();
302 | }
303 | } finally {
304 | if (_didIteratorError2) {
305 | throw _iteratorError2;
306 | }
307 | }
308 | }
309 |
310 | return topics;
311 | }
312 | }]);
313 |
314 | function Topic(label, checkbox, childList, parent, previous) {
315 | _classCallCheck(this, Topic);
316 |
317 | this.label = label;
318 | this.checkbox = checkbox;
319 | this.parent = parent;
320 | this.children = Topic.fromList(childList, this);
321 | this.previous = previous;
322 |
323 | if (this.checkbox.checked) {
324 | this.select();
325 | } else {
326 | this.selected = false;
327 | }
328 | }
329 |
330 | _createClass(Topic, [{
331 | key: 'parentOf',
332 | value: function parentOf(other) {
333 | if (!other) {
334 | return false;
335 | }
336 |
337 | var _iteratorNormalCompletion3 = true;
338 | var _didIteratorError3 = false;
339 | var _iteratorError3 = undefined;
340 |
341 | try {
342 | for (var _iterator3 = this.children[Symbol.iterator](), _step3; !(_iteratorNormalCompletion3 = (_step3 = _iterator3.next()).done); _iteratorNormalCompletion3 = true) {
343 | var topic = _step3.value;
344 |
345 | if (topic === other || topic.parentOf(other)) {
346 | return true;
347 | }
348 | }
349 | } catch (err) {
350 | _didIteratorError3 = true;
351 | _iteratorError3 = err;
352 | } finally {
353 | try {
354 | if (!_iteratorNormalCompletion3 && _iterator3.return) {
355 | _iterator3.return();
356 | }
357 | } finally {
358 | if (_didIteratorError3) {
359 | throw _iteratorError3;
360 | }
361 | }
362 | }
363 |
364 | return false;
365 | }
366 | }, {
367 | key: 'withParents',
368 | value: function withParents() {
369 | return this.parents.concat([this]);
370 | }
371 | }, {
372 | key: 'select',
373 | value: function select() {
374 | // if already selected or a child is selected do nothing
375 | if (this.selected || this.selectedChildren.length) {
376 | return;
377 | }
378 | this.selected = true;
379 | this.checkbox.checked = true;
380 | if (this.parent) {
381 | this.parent.childWasSelected();
382 | }
383 | }
384 | }, {
385 | key: 'deselect',
386 | value: function deselect() {
387 | var selectParent = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : true;
388 |
389 | // if this item is selected explicitly we can deselect it
390 | if (this.selected) {
391 | this.deselectSelfAndParents();
392 | } else {
393 | // otherwise we need to find the selected children to start deselecting
394 | var selectedChildren = this.selectedChildren;
395 |
396 | // if we have none it's a no-op
397 | if (!selectedChildren.length) {
398 | return;
399 | }
400 |
401 | var _iteratorNormalCompletion4 = true;
402 | var _didIteratorError4 = false;
403 | var _iteratorError4 = undefined;
404 |
405 | try {
406 | for (var _iterator4 = selectedChildren[Symbol.iterator](), _step4; !(_iteratorNormalCompletion4 = (_step4 = _iterator4.next()).done); _iteratorNormalCompletion4 = true) {
407 | var child = _step4.value;
408 |
409 | child.deselect(false);
410 | }
411 | } catch (err) {
412 | _didIteratorError4 = true;
413 | _iteratorError4 = err;
414 | } finally {
415 | try {
416 | if (!_iteratorNormalCompletion4 && _iterator4.return) {
417 | _iterator4.return();
418 | }
419 | } finally {
420 | if (_didIteratorError4) {
421 | throw _iteratorError4;
422 | }
423 | }
424 | }
425 | }
426 |
427 | if (selectParent && this.parent) {
428 | this.parent.select();
429 | }
430 | }
431 | }, {
432 | key: 'deselectSelfAndParents',
433 | value: function deselectSelfAndParents() {
434 | var _iteratorNormalCompletion5 = true;
435 | var _didIteratorError5 = false;
436 | var _iteratorError5 = undefined;
437 |
438 | try {
439 | // loop through the parents only deselecting items that don't have other
440 | // selected children
441 | for (var _iterator5 = this.withParents().reverse()[Symbol.iterator](), _step5; !(_iteratorNormalCompletion5 = (_step5 = _iterator5.next()).done); _iteratorNormalCompletion5 = true) {
442 | var topic = _step5.value;
443 |
444 | if (topic.selectedChildren.length) {
445 | break;
446 | } else {
447 | topic.selected = false;
448 | topic.checkbox.checked = false;
449 | }
450 | }
451 | } catch (err) {
452 | _didIteratorError5 = true;
453 | _iteratorError5 = err;
454 | } finally {
455 | try {
456 | if (!_iteratorNormalCompletion5 && _iterator5.return) {
457 | _iterator5.return();
458 | }
459 | } finally {
460 | if (_didIteratorError5) {
461 | throw _iteratorError5;
462 | }
463 | }
464 | }
465 | }
466 | }, {
467 | key: 'childWasSelected',
468 | value: function childWasSelected() {
469 | this.checkbox.checked = true;
470 | this.selected = false;
471 | if (this.parent) {
472 | this.parent.childWasSelected();
473 | }
474 | }
475 | }, {
476 | key: 'topicName',
477 | get: function get() {
478 | return this.label.textContent.replace(/(^\s+|\s+$)/g, '');
479 | }
480 | }, {
481 | key: 'topicNames',
482 | get: function get() {
483 | var items = [];
484 | var _iteratorNormalCompletion6 = true;
485 | var _didIteratorError6 = false;
486 | var _iteratorError6 = undefined;
487 |
488 | try {
489 | for (var _iterator6 = this.parents[Symbol.iterator](), _step6; !(_iteratorNormalCompletion6 = (_step6 = _iterator6.next()).done); _iteratorNormalCompletion6 = true) {
490 | var parent = _step6.value;
491 |
492 | items.push(parent.topicName);
493 | }
494 | } catch (err) {
495 | _didIteratorError6 = true;
496 | _iteratorError6 = err;
497 | } finally {
498 | try {
499 | if (!_iteratorNormalCompletion6 && _iterator6.return) {
500 | _iterator6.return();
501 | }
502 | } finally {
503 | if (_didIteratorError6) {
504 | throw _iteratorError6;
505 | }
506 | }
507 | }
508 |
509 | items.push(this.topicName);
510 | return items;
511 | }
512 | }, {
513 | key: 'selectedChildren',
514 | get: function get() {
515 | return this.children.reduce(function (memo, topic) {
516 | var selected = topic.selectedChildren;
517 | if (topic.selected) {
518 | selected.push(topic);
519 | }
520 | return memo.concat(selected);
521 | }, []);
522 | }
523 | }, {
524 | key: 'parents',
525 | get: function get() {
526 | if (this.parent) {
527 | return this.parent.parents.concat([this.parent]);
528 | } else {
529 | return [];
530 | }
531 | }
532 | }, {
533 | key: 'flattenedChildren',
534 | get: function get() {
535 | return this.children.reduce(function (memo, topic) {
536 | memo.push(topic);
537 | return memo.concat(topic.flattenedChildren);
538 | }, []);
539 | }
540 | }]);
541 |
542 | return Topic;
543 | }();
544 |
545 | var MillerColumnsElement = function (_CustomElement2) {
546 | _inherits(MillerColumnsElement, _CustomElement2);
547 |
548 | function MillerColumnsElement() {
549 | _classCallCheck(this, MillerColumnsElement);
550 |
551 | var _this = _possibleConstructorReturn(this, (MillerColumnsElement.__proto__ || Object.getPrototypeOf(MillerColumnsElement)).call(this));
552 |
553 | _this.classNames = {
554 | column: 'miller-columns__column',
555 | columnHeading: 'miller-columns__column-heading',
556 | backLink: 'govuk-back-link',
557 | columnList: 'miller-columns__column-list',
558 | columnCollapse: 'miller-columns__column--collapse',
559 | columnMedium: 'miller-columns__column--medium',
560 | columnNarrow: 'miller-columns__column--narrow',
561 | columnActive: 'miller-columns__column--active',
562 | item: 'miller-columns__item',
563 | itemParent: 'miller-columns__item--parent',
564 | itemActive: 'miller-columns__item--active',
565 | itemSelected: 'miller-columns__item--selected'
566 | };
567 | return _this;
568 | }
569 |
570 | _createClass(MillerColumnsElement, [{
571 | key: 'connectedCallback',
572 | value: function connectedCallback() {
573 | this.describedbyId = this.getAttribute('aria-describedby');
574 |
575 | var source = document.getElementById(this.getAttribute('for') || '');
576 | if (source) {
577 | this.taxonomy = new Taxonomy(Topic.fromList(source), this);
578 | this.renderTaxonomyColumn(this.taxonomy.topics, true);
579 | this.update();
580 | if (source.parentNode) {
581 | source.parentNode.removeChild(source);
582 | }
583 | this.style.display = 'block';
584 | }
585 | }
586 | }, {
587 | key: 'renderTaxonomyColumn',
588 | value: function renderTaxonomyColumn(topics) {
589 | var _this2 = this;
590 |
591 | var root = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : false;
592 |
593 | var div = document.createElement('div');
594 |
595 | if (!root) {
596 | // Append back link
597 | var backLink = document.createElement('button');
598 | backLink.className = this.classNames.backLink;
599 | backLink.type = 'button';
600 | backLink.innerHTML = 'Back';
601 | backLink.addEventListener('click', function () {
602 | if (topics[0].parent) {
603 | _this2.showCurrentColumns(topics[0].parent.parent);
604 | }
605 | }, false);
606 | div.appendChild(backLink);
607 |
608 | // Append heading
609 | var h3 = document.createElement('h3');
610 | h3.className = this.classNames.columnHeading;
611 | var parentTopicName = topics[0].parent ? topics[0].parent.topicName : null;
612 | if (parentTopicName) {
613 | h3.innerHTML = parentTopicName;
614 | }
615 | div.appendChild(h3);
616 | }
617 |
618 | // Append list
619 | var ul = document.createElement('ul');
620 | ul.className = this.classNames.columnList;
621 | div.className = this.classNames.column;
622 | if (root) {
623 | div.dataset.root = 'true';
624 | } else {
625 | div.classList.add(this.classNames.columnCollapse);
626 | }
627 | div.appendChild(ul);
628 |
629 | // Append column
630 | this.appendChild(div);
631 |
632 | var _iteratorNormalCompletion7 = true;
633 | var _didIteratorError7 = false;
634 | var _iteratorError7 = undefined;
635 |
636 | try {
637 | for (var _iterator7 = topics[Symbol.iterator](), _step7; !(_iteratorNormalCompletion7 = (_step7 = _iterator7.next()).done); _iteratorNormalCompletion7 = true) {
638 | var topic = _step7.value;
639 |
640 | this.renderTopic(topic, ul);
641 | }
642 | } catch (err) {
643 | _didIteratorError7 = true;
644 | _iteratorError7 = err;
645 | } finally {
646 | try {
647 | if (!_iteratorNormalCompletion7 && _iterator7.return) {
648 | _iterator7.return();
649 | }
650 | } finally {
651 | if (_didIteratorError7) {
652 | throw _iteratorError7;
653 | }
654 | }
655 | }
656 | }
657 | }, {
658 | key: 'renderTopic',
659 | value: function renderTopic(topic, list) {
660 | var li = document.createElement('li');
661 | li.classList.add(this.classNames.item);
662 | li.classList.add('govuk-checkboxes--small');
663 | if (this.describedbyId) {
664 | li.setAttribute('aria-describedby', this.describedbyId);
665 | }
666 |
667 | var div = document.createElement('div');
668 | div.className = 'govuk-checkboxes__item';
669 | div.appendChild(topic.checkbox);
670 | div.appendChild(topic.label);
671 | li.appendChild(div);
672 | list.appendChild(li);
673 | this.attachEvents(li, topic);
674 |
675 | if (topic.children.length) {
676 | li.classList.add(this.classNames.itemParent);
677 | this.renderTaxonomyColumn(topic.children);
678 | }
679 | }
680 | }, {
681 | key: 'focusTopic',
682 | value: function focusTopic(topic) {
683 | if (topic instanceof Topic && topic.checkbox) {
684 | var item = topic.checkbox.closest('.' + this.classNames.item);
685 | if (item instanceof HTMLElement) {
686 | item.focus();
687 | }
688 | }
689 | }
690 | }, {
691 | key: 'attachEvents',
692 | value: function attachEvents(trigger, topic) {
693 | var _this3 = this;
694 |
695 | trigger.tabIndex = 0;
696 | trigger.addEventListener('click', function () {
697 | _this3.taxonomy.topicClicked(topic);
698 | topic.checkbox.dispatchEvent(new Event('click'));
699 | }, false);
700 | trigger.addEventListener('keydown', function (event) {
701 | switch (event.key) {
702 | case ' ':
703 | case 'Enter':
704 | event.preventDefault();
705 | _this3.taxonomy.topicClicked(topic);
706 | topic.checkbox.dispatchEvent(new Event('click'));
707 | break;
708 | case 'ArrowUp':
709 | event.preventDefault();
710 | if (topic.previous) {
711 | _this3.showCurrentColumns(topic.previous);
712 | _this3.focusTopic(topic.previous);
713 | }
714 | break;
715 | case 'ArrowDown':
716 | event.preventDefault();
717 | if (topic.next) {
718 | _this3.showCurrentColumns(topic.next);
719 | _this3.focusTopic(topic.next);
720 | }
721 | break;
722 | case 'ArrowLeft':
723 | event.preventDefault();
724 | if (topic.parent) {
725 | _this3.showCurrentColumns(topic.parent);
726 | _this3.focusTopic(topic.parent);
727 | }
728 | break;
729 | case 'ArrowRight':
730 | event.preventDefault();
731 | if (topic.children) {
732 | _this3.showCurrentColumns(topic.children[0]);
733 | _this3.focusTopic(topic.children[0]);
734 | }
735 | break;
736 | default:
737 | return;
738 | }
739 | }, false);
740 | }
741 | }, {
742 | key: 'update',
743 | value: function update() {
744 | this.showSelectedTopics(this.taxonomy.selectedTopics);
745 | this.showActiveTopic(this.taxonomy.active);
746 | this.showCurrentColumns(this.taxonomy.active);
747 |
748 | if (this.selectedElement) {
749 | this.selectedElement.update(this.taxonomy);
750 | }
751 | }
752 | }, {
753 | key: 'updateClassName',
754 | value: function updateClassName(className, items) {
755 | var currentlyWithClass = nodesToArray(this.getElementsByClassName(className));
756 |
757 | var _iteratorNormalCompletion8 = true;
758 | var _didIteratorError8 = false;
759 | var _iteratorError8 = undefined;
760 |
761 | try {
762 | for (var _iterator8 = currentlyWithClass.concat(items)[Symbol.iterator](), _step8; !(_iteratorNormalCompletion8 = (_step8 = _iterator8.next()).done); _iteratorNormalCompletion8 = true) {
763 | var item = _step8.value;
764 |
765 | if (!item) {
766 | continue;
767 | }
768 |
769 | if (items.indexOf(item) !== -1) {
770 | item.classList.add(className);
771 | } else {
772 | item.classList.remove(className);
773 | }
774 | }
775 | } catch (err) {
776 | _didIteratorError8 = true;
777 | _iteratorError8 = err;
778 | } finally {
779 | try {
780 | if (!_iteratorNormalCompletion8 && _iterator8.return) {
781 | _iterator8.return();
782 | }
783 | } finally {
784 | if (_didIteratorError8) {
785 | throw _iteratorError8;
786 | }
787 | }
788 | }
789 | }
790 | }, {
791 | key: 'showSelectedTopics',
792 | value: function showSelectedTopics(selectedTopics) {
793 | var _this4 = this;
794 |
795 | var selectedItems = selectedTopics.reduce(function (memo, child) {
796 | var _iteratorNormalCompletion9 = true;
797 | var _didIteratorError9 = false;
798 | var _iteratorError9 = undefined;
799 |
800 | try {
801 | for (var _iterator9 = child.withParents()[Symbol.iterator](), _step9; !(_iteratorNormalCompletion9 = (_step9 = _iterator9.next()).done); _iteratorNormalCompletion9 = true) {
802 | var topic = _step9.value;
803 |
804 | var item = topic.checkbox.closest('.' + _this4.classNames.item);
805 | if (item instanceof HTMLElement) {
806 | memo.push(item);
807 | }
808 | }
809 | } catch (err) {
810 | _didIteratorError9 = true;
811 | _iteratorError9 = err;
812 | } finally {
813 | try {
814 | if (!_iteratorNormalCompletion9 && _iterator9.return) {
815 | _iterator9.return();
816 | }
817 | } finally {
818 | if (_didIteratorError9) {
819 | throw _iteratorError9;
820 | }
821 | }
822 | }
823 |
824 | return memo;
825 | }, []);
826 |
827 | this.updateClassName(this.classNames.itemSelected, selectedItems);
828 | }
829 | }, {
830 | key: 'showActiveTopic',
831 | value: function showActiveTopic(activeTopic) {
832 | var _this5 = this;
833 |
834 | var activeItems = void 0;
835 |
836 | if (!activeTopic) {
837 | activeItems = [];
838 | } else {
839 | activeItems = activeTopic.withParents().reduce(function (memo, topic) {
840 | var item = topic.checkbox.closest('.' + _this5.classNames.item);
841 |
842 | if (item instanceof HTMLElement) {
843 | memo.push(item);
844 | }
845 |
846 | return memo;
847 | }, []);
848 | }
849 | this.updateClassName(this.classNames.itemActive, activeItems);
850 | }
851 | }, {
852 | key: 'showCurrentColumns',
853 | value: function showCurrentColumns(activeTopic) {
854 | var allColumns = nodesToArray(this.getElementsByClassName(this.classNames.column));
855 | var columnsToShow = activeTopic ? this.columnsForActiveTopic(activeTopic) : [allColumns[0]];
856 | var narrowThreshold = Math.max(3, columnsToShow.length - 1);
857 | var showNarrow = columnsToShow.length > narrowThreshold;
858 | var showMedium = showNarrow && narrowThreshold === 3;
859 | var _classNames = this.classNames,
860 | collapseClass = _classNames.columnCollapse,
861 | narrowClass = _classNames.columnNarrow,
862 | mediumClass = _classNames.columnMedium,
863 | activeClass = _classNames.columnActive;
864 | var _iteratorNormalCompletion10 = true;
865 | var _didIteratorError10 = false;
866 | var _iteratorError10 = undefined;
867 |
868 | try {
869 |
870 | for (var _iterator10 = allColumns[Symbol.iterator](), _step10; !(_iteratorNormalCompletion10 = (_step10 = _iterator10.next()).done); _iteratorNormalCompletion10 = true) {
871 | var item = _step10.value;
872 |
873 | if (!item) {
874 | continue;
875 | }
876 |
877 | item.classList.remove(activeClass);
878 | // we always want to show the root column
879 | if (item.dataset.root === 'true') {
880 | item.classList.remove(narrowClass, mediumClass);
881 | if (showMedium) {
882 | item.classList.add(mediumClass);
883 | } else if (showNarrow) {
884 | item.classList.add(narrowClass);
885 | }
886 | if (columnsToShow.length === 1) {
887 | item.classList.add(activeClass);
888 | }
889 | continue;
890 | }
891 |
892 | var index = columnsToShow.indexOf(item);
893 |
894 | if (index === -1) {
895 | // this is not a column to show
896 | item.classList.add(collapseClass);
897 | } else if (showNarrow && index < narrowThreshold) {
898 | // show this column but narrow
899 | item.classList.remove(collapseClass, narrowClass, mediumClass);
900 | if (showMedium) {
901 | item.classList.add(mediumClass);
902 | } else if (showNarrow) {
903 | item.classList.add(narrowClass);
904 | }
905 | } else {
906 | // show this column in all it's glory
907 | item.classList.remove(collapseClass, narrowClass, mediumClass);
908 | }
909 |
910 | // mark last visible column as active
911 | if (item === columnsToShow[columnsToShow.length - 1]) {
912 | item.classList.add(activeClass);
913 | }
914 | }
915 | } catch (err) {
916 | _didIteratorError10 = true;
917 | _iteratorError10 = err;
918 | } finally {
919 | try {
920 | if (!_iteratorNormalCompletion10 && _iterator10.return) {
921 | _iterator10.return();
922 | }
923 | } finally {
924 | if (_didIteratorError10) {
925 | throw _iteratorError10;
926 | }
927 | }
928 | }
929 | }
930 | }, {
931 | key: 'columnsForActiveTopic',
932 | value: function columnsForActiveTopic(activeTopic) {
933 | if (!activeTopic) {
934 | return [];
935 | }
936 |
937 | var columnSelector = '.' + this.classNames.column;
938 | var columns = activeTopic.withParents().reduce(function (memo, topic) {
939 | var column = topic.checkbox.closest(columnSelector);
940 | if (column instanceof HTMLElement) {
941 | memo.push(column);
942 | }
943 |
944 | return memo;
945 | }, []);
946 |
947 | // we'll want to show the next column too for the next choices
948 | if (activeTopic.children.length) {
949 | var nextColumn = activeTopic.children[0].checkbox.closest(columnSelector);
950 | if (nextColumn instanceof HTMLElement) {
951 | columns.push(nextColumn);
952 | }
953 | }
954 | return columns;
955 | }
956 | }, {
957 | key: 'selectedElement',
958 | get: function get() {
959 | var selected = document.getElementById(this.getAttribute('selected') || '');
960 | return selected instanceof MillerColumnsSelectedElement ? selected : null;
961 | }
962 | }]);
963 |
964 | return MillerColumnsElement;
965 | }(_CustomElement);
966 |
967 | var MillerColumnsSelectedElement = function (_CustomElement3) {
968 | _inherits(MillerColumnsSelectedElement, _CustomElement3);
969 |
970 | function MillerColumnsSelectedElement() {
971 | _classCallCheck(this, MillerColumnsSelectedElement);
972 |
973 | return _possibleConstructorReturn(this, (MillerColumnsSelectedElement.__proto__ || Object.getPrototypeOf(MillerColumnsSelectedElement)).call(this));
974 | }
975 |
976 | _createClass(MillerColumnsSelectedElement, [{
977 | key: 'connectedCallback',
978 | value: function connectedCallback() {
979 | this.list = document.createElement('ol');
980 | this.list.className = 'miller-columns-selected__list';
981 | this.appendChild(this.list);
982 | if (this.millerColumnsElement && this.millerColumnsElement.taxonomy) {
983 | this.update(this.millerColumnsElement.taxonomy);
984 | }
985 | }
986 | }, {
987 | key: 'update',
988 | value: function update(taxonomy) {
989 | this.taxonomy = taxonomy;
990 | var selectedTopics = taxonomy.selectedTopics;
991 | // seems simpler to nuke the list and re-build it
992 | while (this.list.lastChild) {
993 | this.list.removeChild(this.list.lastChild);
994 | }
995 |
996 | if (selectedTopics.length) {
997 | var _iteratorNormalCompletion11 = true;
998 | var _didIteratorError11 = false;
999 | var _iteratorError11 = undefined;
1000 |
1001 | try {
1002 | for (var _iterator11 = selectedTopics[Symbol.iterator](), _step11; !(_iteratorNormalCompletion11 = (_step11 = _iterator11.next()).done); _iteratorNormalCompletion11 = true) {
1003 | var topic = _step11.value;
1004 |
1005 | this.addSelectedTopic(topic);
1006 | }
1007 | } catch (err) {
1008 | _didIteratorError11 = true;
1009 | _iteratorError11 = err;
1010 | } finally {
1011 | try {
1012 | if (!_iteratorNormalCompletion11 && _iterator11.return) {
1013 | _iterator11.return();
1014 | }
1015 | } finally {
1016 | if (_didIteratorError11) {
1017 | throw _iteratorError11;
1018 | }
1019 | }
1020 | }
1021 | } else {
1022 | var li = document.createElement('li');
1023 | li.className = 'miller-columns-selected__list-item';
1024 | li.textContent = 'No selected topics';
1025 | this.list.appendChild(li);
1026 | }
1027 | }
1028 | }, {
1029 | key: 'addSelectedTopic',
1030 | value: function addSelectedTopic(topic) {
1031 | var li = document.createElement('li');
1032 | li.className = 'miller-columns-selected__list-item';
1033 | li.appendChild(this.breadcrumbsElement(topic));
1034 | li.appendChild(this.removeTopicElement(topic));
1035 | this.list.appendChild(li);
1036 | }
1037 | }, {
1038 | key: 'breadcrumbsElement',
1039 | value: function breadcrumbsElement(topic) {
1040 | var div = document.createElement('div');
1041 | div.className = 'govuk-breadcrumbs';
1042 | var ol = document.createElement('ol');
1043 | ol.className = 'govuk-breadcrumbs__list';
1044 | var _iteratorNormalCompletion12 = true;
1045 | var _didIteratorError12 = false;
1046 | var _iteratorError12 = undefined;
1047 |
1048 | try {
1049 | for (var _iterator12 = topic.withParents()[Symbol.iterator](), _step12; !(_iteratorNormalCompletion12 = (_step12 = _iterator12.next()).done); _iteratorNormalCompletion12 = true) {
1050 | var current = _step12.value;
1051 |
1052 | var li = document.createElement('li');
1053 | li.className = 'govuk-breadcrumbs__list-item';
1054 | li.textContent = current.label.textContent;
1055 | ol.appendChild(li);
1056 | }
1057 | } catch (err) {
1058 | _didIteratorError12 = true;
1059 | _iteratorError12 = err;
1060 | } finally {
1061 | try {
1062 | if (!_iteratorNormalCompletion12 && _iterator12.return) {
1063 | _iterator12.return();
1064 | }
1065 | } finally {
1066 | if (_didIteratorError12) {
1067 | throw _iteratorError12;
1068 | }
1069 | }
1070 | }
1071 |
1072 | div.appendChild(ol);
1073 | return div;
1074 | }
1075 | }, {
1076 | key: 'removeTopicElement',
1077 | value: function removeTopicElement(topic) {
1078 | var _this7 = this;
1079 |
1080 | var button = document.createElement('button');
1081 | button.className = 'miller-columns-selected__remove-topic';
1082 | button.textContent = 'Remove topic';
1083 | button.setAttribute('type', 'button');
1084 | button.addEventListener('click', function () {
1085 | triggerEvent(button, 'remove-topic', topic);
1086 | if (_this7.taxonomy) {
1087 | _this7.taxonomy.removeTopic(topic);
1088 | }
1089 | });
1090 |
1091 | var span = document.createElement('span');
1092 | span.className = 'miller-columns-selected__remove-topic-name';
1093 | span.textContent = ': ' + topic.topicName;
1094 | button.appendChild(span);
1095 |
1096 | return button;
1097 | }
1098 | }, {
1099 | key: 'millerColumnsElement',
1100 | get: function get() {
1101 | var millerColumns = document.getElementById(this.getAttribute('for') || '');
1102 | return millerColumns instanceof MillerColumnsElement ? millerColumns : null;
1103 | }
1104 | }]);
1105 |
1106 | return MillerColumnsSelectedElement;
1107 | }(_CustomElement);
1108 |
1109 | if (!window.customElements.get('miller-columns')) {
1110 | window.MillerColumnsElement = MillerColumnsElement;
1111 | window.customElements.define('miller-columns', MillerColumnsElement);
1112 | }
1113 |
1114 | if (!window.customElements.get('miller-columns-selected')) {
1115 | window.MillerColumnsSelectedElement = MillerColumnsSelectedElement;
1116 | window.customElements.define('miller-columns-selected', MillerColumnsSelectedElement);
1117 | }
1118 |
1119 | exports.MillerColumnsElement = MillerColumnsElement;
1120 | exports.MillerColumnsSelectedElement = MillerColumnsSelectedElement;
1121 | });
1122 |
--------------------------------------------------------------------------------
/dist/miller-columns.css:
--------------------------------------------------------------------------------
1 | .miller-columns-selected {
2 | margin-bottom: 20px;
3 | display: block; }
4 | @media (min-width: 40.0625em) {
5 | .miller-columns-selected {
6 | margin-bottom: 30px; } }
7 | .miller-columns-selected .govuk-breadcrumbs {
8 | display: block;
9 | margin: 0;
10 | margin-right: 110px; }
11 | .miller-columns-selected .govuk-breadcrumbs__list-item {
12 | margin-bottom: 0; }
13 |
14 | .miller-columns-selected__list {
15 | font-family: "GDS Transport", arial, sans-serif;
16 | -webkit-font-smoothing: antialiased;
17 | -moz-osx-font-smoothing: grayscale;
18 | font-weight: 400;
19 | font-size: 14px;
20 | font-size: 0.875rem;
21 | line-height: 1.14286;
22 | color: #0b0c0c;
23 | margin: 0;
24 | padding: 0;
25 | list-style-type: none; }
26 | @media print {
27 | .miller-columns-selected__list {
28 | font-family: sans-serif; } }
29 | @media (min-width: 40.0625em) {
30 | .miller-columns-selected__list {
31 | font-size: 16px;
32 | font-size: 1rem;
33 | line-height: 1.25; } }
34 | @media print {
35 | .miller-columns-selected__list {
36 | font-size: 14pt;
37 | line-height: 1.2; } }
38 | @media print {
39 | .miller-columns-selected__list {
40 | color: #000000; } }
41 |
42 | .miller-columns-selected__list-item {
43 | position: relative;
44 | margin-bottom: 0;
45 | padding: 10px 0;
46 | border-top: 1px solid #b1b4b6; }
47 | .miller-columns-selected__list-item:last-child {
48 | border-bottom: 1px solid #b1b4b6; }
49 |
50 | .miller-columns-selected__remove-topic {
51 | font-family: "GDS Transport", arial, sans-serif;
52 | -webkit-font-smoothing: antialiased;
53 | -moz-osx-font-smoothing: grayscale;
54 | text-decoration: underline;
55 | text-decoration-thickness: max(1px, .0625rem);
56 | text-underline-offset: 0.1em;
57 | font-family: "GDS Transport", arial, sans-serif;
58 | -webkit-font-smoothing: antialiased;
59 | -moz-osx-font-smoothing: grayscale;
60 | font-weight: 400;
61 | font-size: 14px;
62 | font-size: 0.875rem;
63 | line-height: 1.14286;
64 | position: absolute;
65 | top: 10px;
66 | right: 0;
67 | margin: 0;
68 | padding: 0;
69 | border: 0;
70 | color: #1d70b8;
71 | background: transparent;
72 | cursor: pointer; }
73 | @media print {
74 | .miller-columns-selected__remove-topic {
75 | font-family: sans-serif; } }
76 | .miller-columns-selected__remove-topic:hover {
77 | text-decoration-thickness: max(3px, .1875rem, .12em); }
78 | .miller-columns-selected__remove-topic:focus {
79 | outline: 3px solid transparent;
80 | color: #0b0c0c;
81 | background-color: #ffdd00;
82 | box-shadow: 0 -2px #ffdd00, 0 4px #0b0c0c;
83 | text-decoration: none; }
84 | .miller-columns-selected__remove-topic:link {
85 | color: #1d70b8; }
86 | .miller-columns-selected__remove-topic:visited {
87 | color: #1d70b8; }
88 | .miller-columns-selected__remove-topic:hover {
89 | color: #003078; }
90 | .miller-columns-selected__remove-topic:active {
91 | color: #0b0c0c; }
92 | .miller-columns-selected__remove-topic:focus {
93 | color: #0b0c0c; }
94 | @media print {
95 | .miller-columns-selected__remove-topic {
96 | font-family: sans-serif; } }
97 | @media (min-width: 40.0625em) {
98 | .miller-columns-selected__remove-topic {
99 | font-size: 16px;
100 | font-size: 1rem;
101 | line-height: 1.25; } }
102 | @media print {
103 | .miller-columns-selected__remove-topic {
104 | font-size: 14pt;
105 | line-height: 1.2; } }
106 | .miller-columns-selected__remove-topic:focus {
107 | outline: 3px solid transparent;
108 | color: #0b0c0c;
109 | background-color: #ffdd00;
110 | box-shadow: 0 -2px #ffdd00, 0 4px #0b0c0c;
111 | text-decoration: none; }
112 |
113 | .miller-columns-selected__remove-topic-name {
114 | position: absolute !important;
115 | width: 1px !important;
116 | height: 1px !important;
117 | margin: 0 !important;
118 | padding: 0 !important;
119 | overflow: hidden !important;
120 | clip: rect(0 0 0 0) !important;
121 | -webkit-clip-path: inset(50%) !important;
122 | clip-path: inset(50%) !important;
123 | border: 0 !important;
124 | white-space: nowrap !important; }
125 |
126 | .js-enabled .miller-columns {
127 | display: none; }
128 | .js-enabled .miller-columns .govuk-checkboxes__input,
129 | .js-enabled .miller-columns .govuk-checkboxes__label {
130 | pointer-events: none; }
131 |
132 | .miller-columns {
133 | display: block;
134 | width: 100%;
135 | height: 100%;
136 | margin-bottom: 20px;
137 | outline: 0;
138 | font-size: 0;
139 | white-space: nowrap; }
140 | @media (min-width: 40.0625em) {
141 | .miller-columns {
142 | margin-bottom: 30px; } }
143 |
144 | .miller-columns__column {
145 | display: none;
146 | width: 100%;
147 | height: 100%;
148 | vertical-align: top;
149 | white-space: normal;
150 | transition-duration: 400ms;
151 | transition-property: width; }
152 | .miller-columns__column.miller-columns__column--active {
153 | display: inline-block; }
154 | @media (min-width: 40.0625em) {
155 | .miller-columns__column {
156 | display: inline-block;
157 | width: 33.3%;
158 | border-right: 1px solid #b1b4b6; } }
159 |
160 | .miller-columns__column--narrow {
161 | width: 16.6%;
162 | overflow-x: hidden; }
163 | .miller-columns__column--narrow .miller-columns__item,
164 | .miller-columns__column--narrow .miller-columns__item label {
165 | white-space: nowrap; }
166 | .miller-columns__column--narrow .miller-columns__item--parent:after {
167 | display: none; }
168 |
169 | .miller-columns__column--medium {
170 | width: 22.2%;
171 | overflow-x: hidden; }
172 | .miller-columns__column--medium .miller-columns__item,
173 | .miller-columns__column--medium .miller-columns__item label {
174 | white-space: nowrap; }
175 | .miller-columns__column--medium .miller-columns__item--parent:after {
176 | display: none; }
177 |
178 | .miller-columns__column--collapse {
179 | display: none; }
180 |
181 | .miller-columns__column-heading {
182 | color: #0b0c0c;
183 | font-family: "GDS Transport", arial, sans-serif;
184 | -webkit-font-smoothing: antialiased;
185 | -moz-osx-font-smoothing: grayscale;
186 | font-weight: 700;
187 | font-size: 16px;
188 | font-size: 1rem;
189 | line-height: 1.25;
190 | margin-bottom: 15px;
191 | margin-top: 0;
192 | padding: 0; }
193 | @media print {
194 | .miller-columns__column-heading {
195 | color: #000000; } }
196 | @media print {
197 | .miller-columns__column-heading {
198 | font-family: sans-serif; } }
199 | @media (min-width: 40.0625em) {
200 | .miller-columns__column-heading {
201 | font-size: 19px;
202 | font-size: 1.1875rem;
203 | line-height: 1.31579; } }
204 | @media print {
205 | .miller-columns__column-heading {
206 | font-size: 14pt;
207 | line-height: 1.15; } }
208 | @media (min-width: 40.0625em) {
209 | .miller-columns__column-heading {
210 | display: none; } }
211 |
212 | .miller-columns__column-list {
213 | margin: 0;
214 | padding: 0; }
215 |
216 | .miller-columns__item {
217 | position: relative;
218 | margin-bottom: 1px;
219 | padding: 2px 9px;
220 | list-style: none;
221 | color: #0b0c0c;
222 | cursor: pointer;
223 | font-family: "GDS Transport", arial, sans-serif;
224 | -webkit-font-smoothing: antialiased;
225 | -moz-osx-font-smoothing: grayscale;
226 | font-weight: 400;
227 | font-size: 14px;
228 | font-size: 0.875rem;
229 | line-height: 1.14286; }
230 | @media print {
231 | .miller-columns__item {
232 | font-family: sans-serif; } }
233 | @media (min-width: 40.0625em) {
234 | .miller-columns__item {
235 | font-size: 16px;
236 | font-size: 1rem;
237 | line-height: 1.25; } }
238 | @media print {
239 | .miller-columns__item {
240 | font-size: 14pt;
241 | line-height: 1.2; } }
242 | .miller-columns__item:hover {
243 | color: #0b0c0c;
244 | background-color: #b1b4b6; }
245 | .miller-columns__item:focus {
246 | outline: 3px solid transparent;
247 | color: #0b0c0c;
248 | background-color: #ffdd00;
249 | box-shadow: 0 -2px #ffdd00, 0 4px #0b0c0c;
250 | text-decoration: none; }
251 | .miller-columns__item .govuk-checkboxes__item {
252 | float: none; }
253 | .miller-columns__item .govuk-checkboxes__label {
254 | font-family: "GDS Transport", arial, sans-serif;
255 | -webkit-font-smoothing: antialiased;
256 | -moz-osx-font-smoothing: grayscale;
257 | font-weight: 400;
258 | font-size: 14px;
259 | font-size: 0.875rem;
260 | line-height: 1.14286;
261 | padding: 12px 15px 10px 1px; }
262 | @media print {
263 | .miller-columns__item .govuk-checkboxes__label {
264 | font-family: sans-serif; } }
265 | @media (min-width: 40.0625em) {
266 | .miller-columns__item .govuk-checkboxes__label {
267 | font-size: 16px;
268 | font-size: 1rem;
269 | line-height: 1.25; } }
270 | @media print {
271 | .miller-columns__item .govuk-checkboxes__label {
272 | font-size: 14pt;
273 | line-height: 1.2; } }
274 | .miller-columns__item .govuk-checkboxes__item:hover .govuk-checkboxes__input:focus + .govuk-checkboxes__label::before,
275 | .miller-columns__item .govuk-checkboxes__item:hover .govuk-checkboxes__input:not(:disabled) + .govuk-checkboxes__label::before {
276 | box-shadow: none; }
277 |
278 | .miller-columns__item--parent:after {
279 | content: "\203A" / "";
280 | position: absolute;
281 | top: 50%;
282 | right: 5px;
283 | margin-top: -17px;
284 | float: right;
285 | font-size: 24px; }
286 |
287 | .miller-columns__item--selected,
288 | .miller-columns__item--selected:hover {
289 | color: #ffffff;
290 | background-color: #505a5f; }
291 | .miller-columns__item--selected .govuk-checkboxes__label,
292 | .miller-columns__item--selected:hover .govuk-checkboxes__label {
293 | color: #ffffff; }
294 |
295 | .miller-columns__item--selected:focus {
296 | outline: 3px solid transparent;
297 | color: #0b0c0c;
298 | background-color: #ffdd00;
299 | box-shadow: 0 -2px #ffdd00, 0 4px #0b0c0c;
300 | text-decoration: none; }
301 | .miller-columns__item--selected:focus .govuk-checkboxes__label {
302 | color: #0b0c0c; }
303 |
304 | .miller-columns__item--active,
305 | .miller-columns__item--active:hover {
306 | color: #ffffff;
307 | background-color: #1d70b8;
308 | box-shadow: none; }
309 | .miller-columns__item--active .govuk-checkboxes__label,
310 | .miller-columns__item--active:hover .govuk-checkboxes__label {
311 | color: #ffffff; }
312 |
313 | .miller-columns__item--active:focus {
314 | outline: 3px solid transparent;
315 | color: #0b0c0c;
316 | background-color: #ffdd00;
317 | box-shadow: 0 -2px #ffdd00, 0 4px #0b0c0c;
318 | text-decoration: none; }
319 | .miller-columns__item--active:focus .govuk-checkboxes__label {
320 | color: #0b0c0c; }
321 |
322 | .miller-columns .govuk-list .govuk-list {
323 | margin-left: 30px; }
324 |
325 | .miller-columns .govuk-back-link {
326 | margin-bottom: 20px;
327 | padding-top: 0;
328 | padding-right: 0;
329 | padding-bottom: 0;
330 | border: 0;
331 | background: transparent;
332 | text-decoration: underline; }
333 | @media (min-width: 40.0625em) {
334 | .miller-columns .govuk-back-link {
335 | display: none; } }
336 |
--------------------------------------------------------------------------------
/examples.scss:
--------------------------------------------------------------------------------
1 | // Don't include the font since it needs to be served from the node modules
2 | // directory it'll only 404 on apps.
3 | $govuk-include-default-font-face: false;
4 |
5 | // Core styles used for the example pages
6 | @import "node_modules/govuk-frontend/dist/govuk/base";
7 | @import "node_modules/govuk-frontend/dist/govuk/objects/all";
8 | @import "node_modules/govuk-frontend/dist/govuk/core/all";
9 | @import "node_modules/govuk-frontend/dist/govuk/components/checkboxes/checkboxes";
10 | @import "node_modules/govuk-frontend/dist/govuk/components/back-link/back-link";
11 | @import "node_modules/govuk-frontend/dist/govuk/components/breadcrumbs/breadcrumbs";
12 |
13 | // Component specific styles
14 | @import "miller-columns";
15 |
--------------------------------------------------------------------------------
/examples/miller-columns-test.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | govuk-miller-columns examples
7 |
8 |
9 |
10 |
11 |
12 |
13 |
16 | Use the right arrow to explore sub-topics, use the up and down arrows to find other topics.
17 |
18 |
19 |
75 |
76 |
77 |
78 |
79 |
--------------------------------------------------------------------------------
/index.js:
--------------------------------------------------------------------------------
1 | /* @flow */
2 |
3 | function nodesToArray(nodes: NodeList | HTMLCollection): Array {
4 | return Array.prototype.slice.call(nodes)
5 | }
6 |
7 | function triggerEvent(element: HTMLElement, eventName: string, detail: Object) {
8 | const params = {bubbles: true, cancelable: true, detail: detail || null}
9 | let event
10 |
11 | if (typeof window.CustomEvent === 'function') {
12 | event = new window.CustomEvent(eventName, params)
13 | } else {
14 | event = document.createEvent('CustomEvent')
15 | event.initCustomEvent(eventName, params.bubbles, params.cancelable, params.detail)
16 | }
17 |
18 | element.dispatchEvent(event)
19 | }
20 |
21 | /**
22 | * This models the taxonomy shown in the miller columns and the current state
23 | * of it.
24 | * It notifies the miller columns element when it has changed state to update
25 | * the UI
26 | */
27 | class Taxonomy {
28 | topics: Array
29 | millerColumns: MillerColumnsElement
30 | // At any time there is one or no active topic, the active topic determines
31 | // what part of the taxonomy is currently shown to the user (i.e which level)
32 | // if this is null a user is shown the root column
33 | active: ?Topic
34 |
35 | constructor(topics: Array, millerColumns: MillerColumnsElement) {
36 | this.topics = topics
37 | this.millerColumns = millerColumns
38 | this.active = this.selectedTopics[0]
39 | }
40 |
41 | /** fetches all the topics that are currently selected */
42 | get selectedTopics(): Array {
43 | return this.topics.reduce((memo, topic) => {
44 | if (topic.selected) {
45 | memo.push(topic)
46 | }
47 |
48 | return memo.concat(topic.selectedChildren)
49 | }, [])
50 | }
51 |
52 | get flattenedTopics(): Array {
53 | return this.topics.reduce((memo, topic) => {
54 | memo.push(topic)
55 | return memo.concat(topic.flattenedChildren)
56 | }, [])
57 | }
58 |
59 | /** Handler for a topic in the miller columns being clicked */
60 | topicClicked(topic: Topic) {
61 | // if this is the active topic or a parent of it we deselect
62 | if (topic === this.active || topic.parentOf(this.active)) {
63 | topic.deselect(true)
64 | this.active = topic.parent
65 | } else if (topic.selected || topic.selectedChildren.length) {
66 | // if this is a selected topic with children we make it active to allow
67 | // picking the children
68 | if (topic.children.length) {
69 | this.active = topic
70 | } else {
71 | // otherwise we deselect it as we know the user can't be traversing
72 | topic.deselect(true)
73 | this.active = topic.parent
74 | }
75 | } else {
76 | // otherwise this is a new selection
77 | topic.select()
78 | this.active = topic
79 | }
80 | this.millerColumns.update()
81 | }
82 |
83 | /** Handler for when a topic is removed via the selected element */
84 | removeTopic(topic: Topic) {
85 | topic.deselect(false)
86 | // determine which topic to mark as active, if any
87 | this.active = this.determineActiveFromRemoved(topic)
88 | this.millerColumns.update()
89 | }
90 |
91 | /** Calculate most relevant topic to show user after they've removed a topic */
92 | determineActiveFromRemoved(topic: Topic): ?Topic {
93 | // if there is already an active item with selected children lets not
94 | // change anything
95 | if (this.active && (this.active.selected || this.active.selectedChildren.length)) {
96 | return this.active
97 | }
98 |
99 | // see if there is a parent with selected topics, that feels like the most
100 | // natural place to end up
101 | for (const parent of topic.parents.reverse()) {
102 | if (parent.selectedChildren.length) {
103 | return parent
104 | }
105 | }
106 |
107 | // if we've still not got one we'll go for the first selected one
108 | return this.selectedTopics[0]
109 | }
110 | }
111 |
112 | /**
113 | * Represents a single topic in the taxonomy and knows whether it is currently
114 | * selected or not
115 | */
116 | class Topic {
117 | static fromList(list: ?HTMLElement, parent: ?Topic = null) {
118 | const topics = []
119 | if (!list) {
120 | return topics
121 | }
122 |
123 | const children = Array.from(list.children)
124 |
125 | for (const [index, item] of children.entries()) {
126 | const label = item.querySelector('label')
127 | const checkbox = item.querySelector('input')
128 | if (label instanceof HTMLLabelElement && checkbox instanceof HTMLInputElement) {
129 | let childList = item.querySelector('ul')
130 | childList = childList instanceof HTMLUListElement ? childList : null
131 |
132 | checkbox.tabIndex = -1
133 |
134 | const previous = index > 0 ? topics[index - 1] : null
135 |
136 | const topic = new Topic(label, checkbox, childList, parent, previous)
137 |
138 | if (index > 0) {
139 | topics[index - 1].next = topic
140 | }
141 |
142 | topics.push(topic)
143 | }
144 | }
145 |
146 | return topics
147 | }
148 |
149 | label: HTMLLabelElement
150 | checkbox: HTMLInputElement
151 | children: Array
152 | parent: ?Topic
153 | next: ?Topic
154 | previous: ?Topic
155 | // Whether this topic is selected, we only allow one item in a branch of the
156 | // taxonomy to be selected.
157 | // E.g. given education > school > 6th form only one of these can be selected
158 | // at a time and the parents are implicity selected from it
159 | selected: boolean
160 |
161 | constructor(
162 | label: HTMLLabelElement,
163 | checkbox: HTMLInputElement,
164 | childList: ?HTMLUListElement,
165 | parent: ?Topic,
166 | previous: ?Topic
167 | ) {
168 | this.label = label
169 | this.checkbox = checkbox
170 | this.parent = parent
171 | this.children = Topic.fromList(childList, this)
172 | this.previous = previous
173 |
174 | if (this.checkbox.checked) {
175 | this.select()
176 | } else {
177 | this.selected = false
178 | }
179 | }
180 |
181 | get topicName(): string {
182 | return this.label.textContent.replace(/(^\s+|\s+$)/g, '')
183 | }
184 |
185 | get topicNames(): Array {
186 | const items = []
187 | for (const parent of this.parents) {
188 | items.push(parent.topicName)
189 | }
190 | items.push(this.topicName)
191 | return items
192 | }
193 |
194 | /** The presence of selected children determines whether this item is considered selected */
195 | get selectedChildren(): Array {
196 | return this.children.reduce((memo, topic) => {
197 | const selected = topic.selectedChildren
198 | if (topic.selected) {
199 | selected.push(topic)
200 | }
201 | return memo.concat(selected)
202 | }, [])
203 | }
204 |
205 | get parents(): Array {
206 | if (this.parent) {
207 | return this.parent.parents.concat([this.parent])
208 | } else {
209 | return []
210 | }
211 | }
212 |
213 | get flattenedChildren(): Array {
214 | return this.children.reduce((memo, topic) => {
215 | memo.push(topic)
216 | return memo.concat(topic.flattenedChildren)
217 | }, [])
218 | }
219 |
220 | /** Whether this topic is the parent of a different one */
221 | parentOf(other: ?Topic): boolean {
222 | if (!other) {
223 | return false
224 | }
225 |
226 | for (const topic of this.children) {
227 | if (topic === other || topic.parentOf(other)) {
228 | return true
229 | }
230 | }
231 |
232 | return false
233 | }
234 |
235 | withParents(): Array {
236 | return this.parents.concat([this])
237 | }
238 |
239 | /** Attempts to select this topic assuming it's not alrerady selected or has selected children */
240 | select() {
241 | // if already selected or a child is selected do nothing
242 | if (this.selected || this.selectedChildren.length) {
243 | return
244 | }
245 | this.selected = true
246 | this.checkbox.checked = true
247 | if (this.parent) {
248 | this.parent.childWasSelected()
249 | }
250 | }
251 |
252 | /**
253 | * Deselects this topic. If this item is not itself selected but a child of it
254 | * is then it traverses to that child and deselects it.
255 | * Takes an optional argument as to whether to select the parent after deselection
256 | * Doing this allows a user to stay in context of their selection in the miller
257 | * column element as deselecting the whole tree would take them back to root
258 | */
259 | deselect(selectParent: boolean = true) {
260 | // if this item is selected explicitly we can deselect it
261 | if (this.selected) {
262 | this.deselectSelfAndParents()
263 | } else {
264 | // otherwise we need to find the selected children to start deselecting
265 | const selectedChildren = this.selectedChildren
266 |
267 | // if we have none it's a no-op
268 | if (!selectedChildren.length) {
269 | return
270 | }
271 |
272 | for (const child of selectedChildren) {
273 | child.deselect(false)
274 | }
275 | }
276 |
277 | if (selectParent && this.parent) {
278 | this.parent.select()
279 | }
280 | }
281 |
282 | deselectSelfAndParents() {
283 | // loop through the parents only deselecting items that don't have other
284 | // selected children
285 | for (const topic of this.withParents().reverse()) {
286 | if (topic.selectedChildren.length) {
287 | break
288 | } else {
289 | topic.selected = false
290 | topic.checkbox.checked = false
291 | }
292 | }
293 | }
294 |
295 | /** If a child is selected we need to implicitly select all the parents */
296 | childWasSelected() {
297 | this.checkbox.checked = true
298 | this.selected = false
299 | if (this.parent) {
300 | this.parent.childWasSelected()
301 | }
302 | }
303 | }
304 |
305 | class MillerColumnsElement extends HTMLElement {
306 | taxonomy: Taxonomy
307 | classNames: Object
308 | describedbyId: ?string
309 |
310 | constructor() {
311 | super()
312 | this.classNames = {
313 | column: 'miller-columns__column',
314 | columnHeading: 'miller-columns__column-heading',
315 | backLink: 'govuk-back-link',
316 | columnList: 'miller-columns__column-list',
317 | columnCollapse: 'miller-columns__column--collapse',
318 | columnMedium: 'miller-columns__column--medium',
319 | columnNarrow: 'miller-columns__column--narrow',
320 | columnActive: 'miller-columns__column--active',
321 | item: 'miller-columns__item',
322 | itemParent: 'miller-columns__item--parent',
323 | itemActive: 'miller-columns__item--active',
324 | itemSelected: 'miller-columns__item--selected'
325 | }
326 | }
327 |
328 | connectedCallback() {
329 | this.describedbyId = this.getAttribute('aria-describedby')
330 |
331 | const source = document.getElementById(this.getAttribute('for') || '')
332 | if (source) {
333 | this.taxonomy = new Taxonomy(Topic.fromList(source), this)
334 | this.renderTaxonomyColumn(this.taxonomy.topics, true)
335 | this.update()
336 | if (source.parentNode) {
337 | source.parentNode.removeChild(source)
338 | }
339 | this.style.display = 'block'
340 | }
341 | }
342 |
343 | /** Returns the element which shows the selections a user has made */
344 | get selectedElement(): ?MillerColumnsSelectedElement {
345 | const selected = document.getElementById(this.getAttribute('selected') || '')
346 | return selected instanceof MillerColumnsSelectedElement ? selected : null
347 | }
348 |
349 | /** Build and insert a column of the taxonomy */
350 | renderTaxonomyColumn(topics: Array, root: boolean = false) {
351 | const div = document.createElement('div')
352 |
353 | if (!root) {
354 | // Append back link
355 | const backLink = document.createElement('button')
356 | backLink.className = this.classNames.backLink
357 | backLink.type = 'button'
358 | backLink.innerHTML = 'Back'
359 | backLink.addEventListener(
360 | 'click',
361 | () => {
362 | if (topics[0].parent) {
363 | this.showCurrentColumns(topics[0].parent.parent)
364 | }
365 | },
366 | false
367 | )
368 | div.appendChild(backLink)
369 |
370 | // Append heading
371 | const h3 = document.createElement('h3')
372 | h3.className = this.classNames.columnHeading
373 | const parentTopicName = topics[0].parent ? topics[0].parent.topicName : null
374 | if (parentTopicName) {
375 | h3.innerHTML = parentTopicName
376 | }
377 | div.appendChild(h3)
378 | }
379 |
380 | // Append list
381 | const ul = document.createElement('ul')
382 | ul.className = this.classNames.columnList
383 | div.className = this.classNames.column
384 | if (root) {
385 | div.dataset.root = 'true'
386 | } else {
387 | div.classList.add(this.classNames.columnCollapse)
388 | }
389 | div.appendChild(ul)
390 |
391 | // Append column
392 | this.appendChild(div)
393 |
394 | for (const topic of topics) {
395 | this.renderTopic(topic, ul)
396 | }
397 | }
398 |
399 | /** Build and insert a list item for a topic */
400 | renderTopic(topic: Topic, list: HTMLElement) {
401 | const li = document.createElement('li')
402 | li.classList.add(this.classNames.item)
403 | li.classList.add('govuk-checkboxes--small')
404 | if (this.describedbyId) {
405 | li.setAttribute('aria-describedby', this.describedbyId)
406 | }
407 |
408 | const div = document.createElement('div')
409 | div.className = 'govuk-checkboxes__item'
410 | div.appendChild(topic.checkbox)
411 | div.appendChild(topic.label)
412 | li.appendChild(div)
413 | list.appendChild(li)
414 | this.attachEvents(li, topic)
415 |
416 | if (topic.children.length) {
417 | li.classList.add(this.classNames.itemParent)
418 | this.renderTaxonomyColumn(topic.children)
419 | }
420 | }
421 |
422 | /** Focus the miller columns item associated with a topic */
423 | focusTopic(topic: ?Topic) {
424 | if (topic instanceof Topic && topic.checkbox) {
425 | const item = topic.checkbox.closest(`.${this.classNames.item}`)
426 | if (item instanceof HTMLElement) {
427 | item.focus()
428 | }
429 | }
430 | }
431 |
432 | /** Sets up the event handling for a list item and a topic */
433 | attachEvents(trigger: HTMLElement, topic: Topic) {
434 | trigger.tabIndex = 0
435 | trigger.addEventListener(
436 | 'click',
437 | () => {
438 | this.taxonomy.topicClicked(topic)
439 | topic.checkbox.dispatchEvent(new Event('click'))
440 | },
441 | false
442 | )
443 | trigger.addEventListener(
444 | 'keydown',
445 | (event: KeyboardEvent) => {
446 | switch (event.key) {
447 | case ' ':
448 | case 'Enter':
449 | event.preventDefault()
450 | this.taxonomy.topicClicked(topic)
451 | topic.checkbox.dispatchEvent(new Event('click'))
452 | break
453 | case 'ArrowUp':
454 | event.preventDefault()
455 | if (topic.previous) {
456 | this.showCurrentColumns(topic.previous)
457 | this.focusTopic(topic.previous)
458 | }
459 | break
460 | case 'ArrowDown':
461 | event.preventDefault()
462 | if (topic.next) {
463 | this.showCurrentColumns(topic.next)
464 | this.focusTopic(topic.next)
465 | }
466 | break
467 | case 'ArrowLeft':
468 | event.preventDefault()
469 | if (topic.parent) {
470 | this.showCurrentColumns(topic.parent)
471 | this.focusTopic(topic.parent)
472 | }
473 | break
474 | case 'ArrowRight':
475 | event.preventDefault()
476 | if (topic.children) {
477 | this.showCurrentColumns(topic.children[0])
478 | this.focusTopic(topic.children[0])
479 | }
480 | break
481 | default:
482 | return
483 | }
484 | },
485 | false
486 | )
487 | }
488 |
489 | /** Update this element to show a change in the state */
490 | update() {
491 | this.showSelectedTopics(this.taxonomy.selectedTopics)
492 | this.showActiveTopic(this.taxonomy.active)
493 | this.showCurrentColumns(this.taxonomy.active)
494 |
495 | if (this.selectedElement) {
496 | this.selectedElement.update(this.taxonomy)
497 | }
498 | }
499 |
500 | /**
501 | * Utility method to swap class names over for a group of elements
502 | * Takes an array of all elements that should have a class and removes it
503 | * from any other items that have it
504 | */
505 | updateClassName(className: string, items: Array) {
506 | const currentlyWithClass = nodesToArray(this.getElementsByClassName(className))
507 |
508 | for (const item of currentlyWithClass.concat(items)) {
509 | if (!item) {
510 | continue
511 | }
512 |
513 | if (items.indexOf(item) !== -1) {
514 | item.classList.add(className)
515 | } else {
516 | item.classList.remove(className)
517 | }
518 | }
519 | }
520 |
521 | /** Given an array of selected topics update the UI */
522 | showSelectedTopics(selectedTopics: Array) {
523 | const selectedItems = selectedTopics.reduce((memo, child) => {
524 | for (const topic of child.withParents()) {
525 | const item = topic.checkbox.closest(`.${this.classNames.item}`)
526 | if (item instanceof HTMLElement) {
527 | memo.push(item)
528 | }
529 | }
530 |
531 | return memo
532 | }, [])
533 |
534 | this.updateClassName(this.classNames.itemSelected, selectedItems)
535 | }
536 |
537 | /** Update the topic items for the presence (or not) of an active topic */
538 | showActiveTopic(activeTopic: ?Topic) {
539 | let activeItems
540 |
541 | if (!activeTopic) {
542 | activeItems = []
543 | } else {
544 | activeItems = activeTopic.withParents().reduce((memo, topic) => {
545 | const item = topic.checkbox.closest(`.${this.classNames.item}`)
546 |
547 | if (item instanceof HTMLElement) {
548 | memo.push(item)
549 | }
550 |
551 | return memo
552 | }, [])
553 | }
554 | this.updateClassName(this.classNames.itemActive, activeItems)
555 | }
556 |
557 | /** Change what columns are visible based on the active (or not) topic */
558 | showCurrentColumns(activeTopic: ?Topic) {
559 | const allColumns = nodesToArray(this.getElementsByClassName(this.classNames.column))
560 | const columnsToShow = activeTopic ? this.columnsForActiveTopic(activeTopic) : [allColumns[0]]
561 | const narrowThreshold = Math.max(3, columnsToShow.length - 1)
562 | const showNarrow = columnsToShow.length > narrowThreshold
563 | const showMedium = showNarrow && narrowThreshold === 3
564 | const {
565 | columnCollapse: collapseClass,
566 | columnNarrow: narrowClass,
567 | columnMedium: mediumClass,
568 | columnActive: activeClass
569 | } = this.classNames
570 |
571 | for (const item of allColumns) {
572 | if (!item) {
573 | continue
574 | }
575 |
576 | item.classList.remove(activeClass)
577 | // we always want to show the root column
578 | if (item.dataset.root === 'true') {
579 | item.classList.remove(narrowClass, mediumClass)
580 | if (showMedium) {
581 | item.classList.add(mediumClass)
582 | } else if (showNarrow) {
583 | item.classList.add(narrowClass)
584 | }
585 | if (columnsToShow.length === 1) {
586 | item.classList.add(activeClass)
587 | }
588 | continue
589 | }
590 |
591 | const index = columnsToShow.indexOf(item)
592 |
593 | if (index === -1) {
594 | // this is not a column to show
595 | item.classList.add(collapseClass)
596 | } else if (showNarrow && index < narrowThreshold) {
597 | // show this column but narrow
598 | item.classList.remove(collapseClass, narrowClass, mediumClass)
599 | if (showMedium) {
600 | item.classList.add(mediumClass)
601 | } else if (showNarrow) {
602 | item.classList.add(narrowClass)
603 | }
604 | } else {
605 | // show this column in all it's glory
606 | item.classList.remove(collapseClass, narrowClass, mediumClass)
607 | }
608 |
609 | // mark last visible column as active
610 | if (item === columnsToShow[columnsToShow.length - 1]) {
611 | item.classList.add(activeClass)
612 | }
613 | }
614 | }
615 |
616 | /** Determine which columns should be shown based on the active topic */
617 | columnsForActiveTopic(activeTopic: ?Topic): Array {
618 | if (!activeTopic) {
619 | return []
620 | }
621 |
622 | const columnSelector = `.${this.classNames.column}`
623 | const columns = activeTopic.withParents().reduce((memo, topic) => {
624 | const column = topic.checkbox.closest(columnSelector)
625 | if (column instanceof HTMLElement) {
626 | memo.push(column)
627 | }
628 |
629 | return memo
630 | }, [])
631 |
632 | // we'll want to show the next column too for the next choices
633 | if (activeTopic.children.length) {
634 | const nextColumn = activeTopic.children[0].checkbox.closest(columnSelector)
635 | if (nextColumn instanceof HTMLElement) {
636 | columns.push(nextColumn)
637 | }
638 | }
639 | return columns
640 | }
641 | }
642 |
643 | class MillerColumnsSelectedElement extends HTMLElement {
644 | list: HTMLElement
645 | taxonomy: ?Taxonomy
646 |
647 | constructor() {
648 | super()
649 | }
650 |
651 | connectedCallback() {
652 | this.list = document.createElement('ol')
653 | this.list.className = 'miller-columns-selected__list'
654 | this.appendChild(this.list)
655 | if (this.millerColumnsElement && this.millerColumnsElement.taxonomy) {
656 | this.update(this.millerColumnsElement.taxonomy)
657 | }
658 | }
659 |
660 | get millerColumnsElement(): ?MillerColumnsElement {
661 | const millerColumns = document.getElementById(this.getAttribute('for') || '')
662 | return millerColumns instanceof MillerColumnsElement ? millerColumns : null
663 | }
664 |
665 | /** Update the UI to show the selected topics */
666 | update(taxonomy: Taxonomy) {
667 | this.taxonomy = taxonomy
668 | const selectedTopics = taxonomy.selectedTopics
669 | // seems simpler to nuke the list and re-build it
670 | while (this.list.lastChild) {
671 | this.list.removeChild(this.list.lastChild)
672 | }
673 |
674 | if (selectedTopics.length) {
675 | for (const topic of selectedTopics) {
676 | this.addSelectedTopic(topic)
677 | }
678 | } else {
679 | const li = document.createElement('li')
680 | li.className = 'miller-columns-selected__list-item'
681 | li.textContent = 'No selected topics'
682 | this.list.appendChild(li)
683 | }
684 | }
685 |
686 | addSelectedTopic(topic: Topic) {
687 | const li = document.createElement('li')
688 | li.className = 'miller-columns-selected__list-item'
689 | li.appendChild(this.breadcrumbsElement(topic))
690 | li.appendChild(this.removeTopicElement(topic))
691 | this.list.appendChild(li)
692 | }
693 |
694 | breadcrumbsElement(topic: Topic): HTMLElement {
695 | const div = document.createElement('div')
696 | div.className = 'govuk-breadcrumbs'
697 | const ol = document.createElement('ol')
698 | ol.className = 'govuk-breadcrumbs__list'
699 | for (const current of topic.withParents()) {
700 | const li = document.createElement('li')
701 | li.className = 'govuk-breadcrumbs__list-item'
702 | li.textContent = current.label.textContent
703 | ol.appendChild(li)
704 | }
705 | div.appendChild(ol)
706 | return div
707 | }
708 |
709 | removeTopicElement(topic: Topic): HTMLElement {
710 | const button = document.createElement('button')
711 | button.className = 'miller-columns-selected__remove-topic'
712 | button.textContent = 'Remove topic'
713 | button.setAttribute('type', 'button')
714 | button.addEventListener('click', () => {
715 | triggerEvent(button, 'remove-topic', topic)
716 | if (this.taxonomy) {
717 | this.taxonomy.removeTopic(topic)
718 | }
719 | })
720 |
721 | const span = document.createElement('span')
722 | span.className = 'miller-columns-selected__remove-topic-name'
723 | span.textContent = `: ${topic.topicName}`
724 | button.appendChild(span)
725 |
726 | return button
727 | }
728 | }
729 |
730 | if (!window.customElements.get('miller-columns')) {
731 | window.MillerColumnsElement = MillerColumnsElement
732 | window.customElements.define('miller-columns', MillerColumnsElement)
733 | }
734 |
735 | if (!window.customElements.get('miller-columns-selected')) {
736 | window.MillerColumnsSelectedElement = MillerColumnsSelectedElement
737 | window.customElements.define('miller-columns-selected', MillerColumnsSelectedElement)
738 | }
739 |
740 | export {MillerColumnsElement, MillerColumnsSelectedElement}
741 |
--------------------------------------------------------------------------------
/miller-columns-selected.scss:
--------------------------------------------------------------------------------
1 | .miller-columns-selected {
2 | @include govuk-responsive-margin(6, "bottom");
3 | display: block;
4 |
5 | .govuk-breadcrumbs {
6 | display: block;
7 | margin: 0;
8 | margin-right: 110px;
9 | }
10 |
11 | .govuk-breadcrumbs__list-item {
12 | margin-bottom: 0;
13 | }
14 | }
15 |
16 | .miller-columns-selected__list {
17 | @include govuk-font($size: 16);
18 | @include govuk-text-colour;
19 | margin: 0;
20 | padding: 0;
21 | list-style-type: none;
22 | }
23 |
24 | .miller-columns-selected__list-item {
25 | position: relative;
26 | margin-bottom: 0;
27 | padding: govuk-spacing(2) 0;
28 | border-top: 1px solid $govuk-border-colour;
29 |
30 | &:last-child {
31 | border-bottom: 1px solid $govuk-border-colour;
32 | }
33 | }
34 |
35 | .miller-columns-selected__remove-topic {
36 | @include govuk-link-common;
37 | @include govuk-link-style-no-visited-state;
38 | @include govuk-font($size: 16);
39 | position: absolute;
40 | top: govuk-spacing(2);
41 | right: 0;
42 | margin: 0;
43 | padding: 0;
44 | border: 0;
45 | color: $govuk-link-colour;
46 | background: transparent;
47 | cursor: pointer;
48 |
49 | &:focus {
50 | @include govuk-focused-text;
51 | }
52 | }
53 |
54 | .miller-columns-selected__remove-topic-name {
55 | @include govuk-visually-hidden;
56 | }
57 |
--------------------------------------------------------------------------------
/miller-columns.scss:
--------------------------------------------------------------------------------
1 | $govuk-include-default-font-face: false;
2 | $govuk-new-link-styles: true;
3 |
4 | @import "node_modules/govuk-frontend/dist/govuk/base";
5 | @import "miller-columns-selected";
6 |
7 | $mc-transition-time: 400ms;
8 | $mc-selected-item-colour: govuk-colour("white");
9 | $mc-selected-item-background: govuk-colour("dark-grey");
10 | $mc-active-item-colour: govuk-colour("white");
11 | $mc-active-item-background: govuk-colour("blue");
12 |
13 | .js-enabled {
14 | // Hide nested lists
15 | .miller-columns {
16 | display: none;
17 |
18 | // Disable pointer events on checkboxes to prevent them from interfering with the miller-columns items
19 | .govuk-checkboxes__input,
20 | .govuk-checkboxes__label {
21 | pointer-events: none;
22 | }
23 | }
24 | }
25 |
26 | .miller-columns {
27 | display: block;
28 | width: 100%;
29 | height: 100%;
30 | @include govuk-responsive-margin(6, "bottom");
31 | outline: 0;
32 | font-size: 0;
33 | white-space: nowrap;
34 | }
35 |
36 | .miller-columns__column {
37 | display: none;
38 | width: 100%;
39 | height: 100%;
40 | vertical-align: top;
41 | white-space: normal;
42 | transition-duration: $mc-transition-time;
43 | transition-property: width;
44 |
45 | &.miller-columns__column--active {
46 | display: inline-block;
47 | }
48 |
49 | @include govuk-media-query($from: tablet) {
50 | display: inline-block;
51 | width: 33.3%;
52 | border-right: 1px solid $govuk-border-colour;
53 | }
54 | }
55 |
56 | .miller-columns__column--narrow {
57 | width: 16.6%;
58 | overflow-x: hidden;
59 |
60 | .miller-columns__item,
61 | .miller-columns__item label {
62 | white-space: nowrap;
63 | }
64 |
65 | .miller-columns__item--parent:after {
66 | display: none;
67 | }
68 | }
69 |
70 | .miller-columns__column--medium {
71 | width: 22.2%;
72 | overflow-x: hidden;
73 |
74 | .miller-columns__item,
75 | .miller-columns__item label {
76 | white-space: nowrap;
77 | }
78 |
79 | .miller-columns__item--parent:after {
80 | display: none;
81 | }
82 | }
83 |
84 | .miller-columns__column--collapse {
85 | display: none;
86 | }
87 |
88 | .miller-columns__column-heading {
89 | @include govuk-text-colour;
90 | @include govuk-font($size: 19, $weight: bold);
91 | margin-bottom: govuk-spacing(3);
92 | margin-top: 0;
93 | padding: 0;
94 |
95 | @include govuk-media-query($from: tablet) {
96 | display: none;
97 | }
98 | }
99 |
100 | .miller-columns__column-list {
101 | margin: 0;
102 | padding: 0;
103 | }
104 |
105 | .miller-columns__item {
106 | position: relative;
107 | margin-bottom: 1px;
108 | padding: 2px 9px;
109 | list-style: none;
110 | color: $govuk-text-colour;
111 | cursor: pointer;
112 | @include govuk-font($size: 16);
113 |
114 | &:hover {
115 | color: govuk-colour("black");
116 | background-color: $govuk-hover-colour;
117 | }
118 |
119 | &:focus {
120 | @include govuk-focused-text;
121 | }
122 |
123 | .govuk-checkboxes__item {
124 | float: none;
125 | }
126 |
127 | .govuk-checkboxes__label {
128 | @include govuk-font($size: 16);
129 | padding: 12px 15px 10px 1px;
130 | }
131 |
132 | // Remove box-shadow set in govuk-frontend for govuk-checkboxes__input
133 | // as we rely on the parent element miller-columns__item to show the target area on hover
134 | .govuk-checkboxes__item:hover .govuk-checkboxes__input:focus + .govuk-checkboxes__label::before,
135 | .govuk-checkboxes__item:hover .govuk-checkboxes__input:not(:disabled) + .govuk-checkboxes__label::before {
136 | box-shadow: none;
137 | }
138 | }
139 |
140 | .miller-columns__item--parent:after {
141 | content: "\203A" / "";
142 | position: absolute;
143 | top: 50%;
144 | right: 5px;
145 | margin-top: -17px;
146 | float: right;
147 | font-size: 24px;
148 | }
149 |
150 | .miller-columns__item--selected,
151 | .miller-columns__item--selected:hover {
152 | color: $mc-selected-item-colour;
153 | background-color: $mc-selected-item-background;
154 |
155 | .govuk-checkboxes__label {
156 | color: $mc-selected-item-colour;
157 | }
158 | }
159 |
160 | .miller-columns__item--selected:focus {
161 | @include govuk-focused-text;
162 |
163 | .govuk-checkboxes__label {
164 | color: $govuk-text-colour;
165 | }
166 | }
167 |
168 | .miller-columns__item--active,
169 | .miller-columns__item--active:hover {
170 | color: $mc-active-item-colour;
171 | background-color: $mc-active-item-background;
172 | box-shadow: none;
173 |
174 | .govuk-checkboxes__label {
175 | color: $mc-active-item-colour;
176 | }
177 | }
178 |
179 | .miller-columns__item--active:focus {
180 | @include govuk-focused-text;
181 |
182 | .govuk-checkboxes__label {
183 | color: $govuk-text-colour;
184 | }
185 | }
186 |
187 | .miller-columns {
188 | .govuk-list {
189 | .govuk-list {
190 | margin-left: govuk-spacing(6);
191 | }
192 | }
193 |
194 | .govuk-back-link {
195 | margin-bottom: govuk-spacing(4);
196 | padding-top: 0;
197 | padding-right: 0;
198 | padding-bottom: 0;
199 | border: 0;
200 | background: transparent;
201 | text-decoration: underline;
202 |
203 | @include govuk-media-query($from: tablet) {
204 | display: none;
205 | }
206 | }
207 | }
208 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "miller-columns-element",
3 | "version": "2.0.1",
4 | "description": "Miller columns (cascading lists) for hierarchical topic selection on GOV.UK taxonomy",
5 | "main": "dist/index.umd.js",
6 | "module": "dist/index.esm.js",
7 | "license": "MIT",
8 | "repository": "alphagov/miller-columns-element",
9 | "files": [
10 | "dist"
11 | ],
12 | "watch": {
13 | "build": [
14 | "index.js",
15 | "examples.scss",
16 | "miller-columns.scss",
17 | "miller-columns-selected.scss"
18 | ]
19 | },
20 | "scripts": {
21 | "clean": "rm -rf dist",
22 | "lint": "sass-lint examples.scss -v -q && eslint index.js test/ && flow check",
23 | "prebuild": "npm run clean && mkdir dist",
24 | "build-css": "node-sass examples.scss dist/examples.css && node-sass miller-columns.scss dist/miller-columns.css",
25 | "build-umd": "BABEL_ENV=umd babel index.js -o dist/index.umd.js",
26 | "build-esm": "BABEL_ENV=esm babel index.js -o dist/index.esm.js",
27 | "build-examples": "rm -rf examples/dist && cp -r dist examples/dist",
28 | "build": "npm run build-css && npm run build-umd && npm run build-esm && npm run build-examples",
29 | "pretest": "npm run build && npm run lint",
30 | "test": "karma start test/karma.config.js",
31 | "watch": "npm-watch"
32 | },
33 | "devDependencies": {
34 | "babel-cli": "^6.18.0",
35 | "babel-plugin-transform-custom-element-classes": "^0.1.0",
36 | "babel-preset-es2015": "^6.24.1",
37 | "babel-preset-flow": "^6.23.0",
38 | "chai": "^4.2.0",
39 | "eslint": "^7.2.0",
40 | "eslint-plugin-github": "1.6.0",
41 | "eslint-plugin-relay": "^2.0.0",
42 | "flow-bin": "^0.133.0",
43 | "govuk-frontend": "^5.0.0",
44 | "karma": "^6.0.0",
45 | "karma-chai": "^0.1.0",
46 | "karma-chrome-launcher": "^3.1.0",
47 | "karma-mocha": "^2.0.0",
48 | "karma-mocha-reporter": "^2.2.5",
49 | "mocha": "^11.1.0",
50 | "node-sass": "^9.0.0",
51 | "npm-watch": "^0.13.0",
52 | "sass-lint": "^1.13.1"
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/prettier.config.js:
--------------------------------------------------------------------------------
1 | module.exports = require('eslint-plugin-github/prettier.config')
2 |
--------------------------------------------------------------------------------
/test/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "env": {
3 | "mocha": true
4 | },
5 | "globals": {
6 | "assert": true
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/test/karma.config.js:
--------------------------------------------------------------------------------
1 | module.exports = function(config) {
2 | config.set({
3 | frameworks: ['mocha', 'chai'],
4 | files: ['../dist/index.umd.js', 'test.js'],
5 | reporters: ['mocha'],
6 | port: 9876,
7 | colors: true,
8 | logLevel: config.LOG_INFO,
9 | browsers: ['ChromeHeadless'],
10 | autoWatch: false,
11 | singleRun: true,
12 | concurrency: Infinity
13 | })
14 | }
15 |
--------------------------------------------------------------------------------
/test/test.js:
--------------------------------------------------------------------------------
1 | describe('miller-columns', function() {
2 | describe('element creation', function() {
3 | it('creates from document.createElement', function() {
4 | const el = document.createElement('miller-columns')
5 | assert.equal('MILLER-COLUMNS', el.nodeName)
6 | })
7 |
8 | it('creates from constructor', function() {
9 | const el = new window.MillerColumnsElement()
10 | assert.equal('MILLER-COLUMNS', el.nodeName)
11 | })
12 | })
13 |
14 | describe('after tree insertion', function() {
15 | function pressKey(key, element) {
16 | const event = document.createEvent('Event')
17 | event.initEvent('keydown', true, true)
18 | event.key = key
19 | element.dispatchEvent(event)
20 | }
21 |
22 | beforeEach(function() {
23 | const container = document.createElement('div')
24 | container.innerHTML = `
25 |
26 | Use the right arrow to explore sub-topics, use the up and down arrows to find other topics.
27 |
28 |
92 |
93 | `
94 | document.body.append(container)
95 | })
96 |
97 | afterEach(function() {
98 | document.body.innerHTML = undefined
99 | })
100 |
101 | it('unnests lists', function() {
102 | const lists = document.querySelectorAll('ul')
103 | assert.equal(lists.length, 4)
104 | })
105 |
106 | it('marks items with children as parents', function() {
107 | const firstItem = document.querySelector('ul li')
108 | assert.isTrue(firstItem.classList.contains('miller-columns__item--parent'))
109 | })
110 |
111 | it('marks selected item as active when clicked', function() {
112 | const firstItem = document.querySelector('ul li')
113 | const firstItemCheckbox = firstItem.querySelector('input')
114 | firstItemCheckbox.addEventListener('click', function(e) {
115 | assert.deepEqual(e.target, firstItemCheckbox)
116 | })
117 |
118 | firstItem.click()
119 |
120 | assert.isTrue(firstItem.classList.contains('miller-columns__item--active'))
121 | assert.isTrue(firstItem.querySelector('input').checked)
122 | })
123 |
124 | it('marks selected item as active when pressing Enter', function() {
125 | const firstItem = document.querySelector('ul li')
126 | const firstItemCheckbox = firstItem.querySelector('input')
127 | firstItemCheckbox.addEventListener('keydown', function(e) {
128 | assert.deepEqual(e.target, firstItemCheckbox)
129 | })
130 |
131 | firstItem.focus()
132 | pressKey('Enter', firstItemCheckbox)
133 |
134 | assert.isTrue(firstItem.classList.contains('miller-columns__item--active'))
135 | assert.isTrue(firstItem.querySelector('input').checked)
136 | })
137 |
138 | it('marks selected item as active when pressing Space', function() {
139 | const firstItem = document.querySelector('ul li')
140 | const firstItemCheckbox = firstItem.querySelector('input')
141 | firstItemCheckbox.addEventListener('keydown', function(e) {
142 | assert.deepEqual(e.target, firstItemCheckbox)
143 | })
144 |
145 | firstItem.focus()
146 | pressKey(' ', firstItemCheckbox)
147 |
148 | assert.isTrue(firstItem.classList.contains('miller-columns__item--active'))
149 | assert.isTrue(firstItem.querySelector('input').checked)
150 | })
151 |
152 | it('marks next item as focused when pressing ArrowDown', function() {
153 | const firstItem = document.querySelector('.miller-columns__column li:nth-of-type(1)')
154 | const secondItem = document.querySelector('.miller-columns__column li:nth-of-type(2)')
155 |
156 | firstItem.focus()
157 | pressKey('ArrowDown', firstItem)
158 |
159 | assert.deepEqual(secondItem, document.activeElement)
160 | })
161 |
162 | it('marks previous item as focused when pressing ArrowUp', function() {
163 | const firstItem = document.querySelector('.miller-columns__column li:nth-of-type(1)')
164 | const secondItem = document.querySelector('.miller-columns__column li:nth-of-type(2)')
165 |
166 | secondItem.focus()
167 | pressKey('ArrowUp', secondItem)
168 |
169 | assert.deepEqual(firstItem, document.activeElement)
170 | })
171 |
172 | it('marks first child item as focused when pressing ArrowRight', function() {
173 | const firstItemL1 = document.querySelector('.miller-columns__column:nth-of-type(1) li')
174 | const firstItemL2 = document.querySelector('.miller-columns__column:nth-of-type(2) li')
175 |
176 | firstItemL1.focus()
177 | pressKey('ArrowRight', firstItemL1)
178 |
179 | assert.deepEqual(firstItemL2, document.activeElement)
180 | })
181 |
182 | it('marks parent item as focused when pressing ArrowLeft', function() {
183 | const firstItemL1 = document.querySelector('.miller-columns__column:nth-of-type(1) li')
184 | const firstItemL2 = document.querySelector('.miller-columns__column:nth-of-type(2) li')
185 |
186 | firstItemL2.focus()
187 | pressKey('ArrowLeft', firstItemL2)
188 |
189 | assert.deepEqual(firstItemL1, document.activeElement)
190 | })
191 |
192 | it('shows the child list for active list item', function() {
193 | const firstItem = document.querySelector('ul li')
194 | const l2List = document.querySelectorAll('.miller-columns__column')[1]
195 | assert.isTrue(l2List.classList.contains('miller-columns__column--collapse'))
196 | firstItem.click()
197 | assert.isFalse(l2List.classList.contains('miller-columns__column--collapse'))
198 | })
199 |
200 | it('unselects children when item is unselected', function() {
201 | const firstItemL1 = document.querySelector('.miller-columns__column:nth-of-type(1) li')
202 | const firstItemL2 = document.querySelector('.miller-columns__column:nth-of-type(2) li')
203 | firstItemL1.click()
204 | firstItemL2.click()
205 | firstItemL1.click()
206 |
207 | assert.isFalse(firstItemL2.classList.contains('miller-columns__item--selected'))
208 | assert.isFalse(firstItemL2.querySelector('input').checked)
209 | })
210 |
211 | it("doesn't unselect items above the item that was clicked in the tree", function() {
212 | const firstItemL1 = document.querySelector('.miller-columns__column:nth-of-type(1) li')
213 | firstItemL1.click()
214 | const firstItemL2 = document.querySelector(
215 | '.miller-columns__column:not(.miller-columns__column--collapse):nth-of-type(2) li'
216 | )
217 | firstItemL2.click()
218 |
219 | firstItemL2.click()
220 |
221 | assert.isFalse(firstItemL2.classList.contains('miller-columns__item--selected'))
222 | assert.isFalse(firstItemL2.querySelector('input').checked)
223 | assert.isTrue(firstItemL1.classList.contains('miller-columns__item--selected'))
224 | assert.isTrue(firstItemL1.querySelector('input').checked)
225 | })
226 |
227 | it('shows active items in selected items', function() {
228 | const firstItemL1 = document.querySelector('.miller-columns__column:nth-of-type(1) li')
229 | const firstLabelL1 = firstItemL1.querySelector('label')
230 | const firstItemL2 = document.querySelector('.miller-columns__column:nth-of-type(2) li')
231 | const firstLabelL2 = firstItemL2.querySelector('label')
232 | firstItemL1.click()
233 | firstItemL2.click()
234 | const selected = document.querySelector('#selected-items ol')
235 | assert.equal(selected.childNodes.length, 1)
236 | assert.isTrue(selected.textContent.includes(firstLabelL1.textContent))
237 | assert.isTrue(selected.textContent.includes(firstLabelL2.textContent))
238 | })
239 |
240 | it('removes a chain from stored selected items', function() {
241 | const firstItemL1 = document.querySelector('.miller-columns__column li')
242 | const millerColumnsSelected = document.querySelector('miller-columns-selected')
243 | millerColumnsSelected.addEventListener('remove-topic', function(e) {
244 | assert.equal(e.detail.topicName, "Parenting, childcare and children's services")
245 | })
246 |
247 | firstItemL1.click()
248 |
249 | const firstItemRemove = document.querySelector('#selected-items button')
250 | const firstItemRemoveHiddenText = firstItemRemove.querySelector('.miller-columns-selected__remove-topic-name')
251 | assert.include(firstItemRemoveHiddenText.textContent, "Parenting, childcare and children's services")
252 | firstItemRemove.click()
253 |
254 | const selectedItems = document.querySelector('#selected-items')
255 | assert.equal(selectedItems.textContent, 'No selected topics')
256 | })
257 |
258 | it('creates entries of selected item for adjacent topics', function() {
259 | const firstItemL1 = document.querySelector('.miller-columns__column:nth-of-type(1) li')
260 | const firstItemL2 = document.querySelector('.miller-columns__column:nth-of-type(2) li')
261 | const secondItemL2 = document.querySelector('.miller-columns__column:nth-of-type(2) li:nth-of-type(2)')
262 |
263 | const firstLabelL2 = firstItemL2.querySelector('label')
264 | const secondLabelL2 = secondItemL2.querySelector('label')
265 |
266 | firstItemL1.click()
267 | firstItemL2.click()
268 | secondItemL2.click()
269 |
270 | const selectedItems = document.querySelector('#selected-items ol')
271 | assert.equal(selectedItems.childNodes.length, 2)
272 | assert.isTrue(selectedItems.textContent.includes(firstLabelL2.textContent))
273 | assert.isTrue(selectedItems.textContent.includes(secondLabelL2.textContent))
274 | })
275 |
276 | it('provides an API to access the breadcrumb trail of a topic', function() {
277 | const firstItemL2 = document.querySelector('miller-columns').taxonomy.topics[0].children[0]
278 | assert.deepEqual(firstItemL2.topicNames, [
279 | "Parenting, childcare and children's services",
280 | 'Divorce, separation and legal issues'
281 | ])
282 | })
283 |
284 | it('provides an API to access children topics in a flat list', function() {
285 | const firstItemL1 = document.querySelector('miller-columns').taxonomy.topics[0]
286 | assert.equal(firstItemL1.flattenedChildren.length, 5)
287 | })
288 |
289 | it('provides an API to access all topics in a flat list', function() {
290 | const millerColumns = document.querySelector('miller-columns')
291 | assert.equal(millerColumns.taxonomy.flattenedTopics.length, 7)
292 | })
293 |
294 | it('shows active column while selecting items', function() {
295 | const firstColumn = document.querySelectorAll('.miller-columns__column')[0]
296 | const firstItemL1 = firstColumn.querySelector('li')
297 | const secondItemL1 = firstColumn.querySelector('li:nth-of-type(2)')
298 | const secondColumn = document.querySelectorAll('.miller-columns__column')[1]
299 | const firstItemL2 = secondColumn.querySelector('li')
300 | const thirdColumn = document.querySelectorAll('.miller-columns__column')[2]
301 |
302 | assert.equal(document.querySelector('.miller-columns__column--active'), firstColumn)
303 | firstItemL1.click()
304 | assert.equal(document.querySelector('.miller-columns__column--active'), secondColumn)
305 | firstItemL2.click()
306 | assert.equal(document.querySelector('.miller-columns__column--active'), thirdColumn)
307 | secondItemL1.click()
308 | assert.equal(document.querySelector('.miller-columns__column--active'), firstColumn)
309 | })
310 |
311 | it('shows parent element as heading for each column', function() {
312 | const firstColumn = document.querySelectorAll('.miller-columns__column')[0]
313 | const firstLabelL1 = firstColumn.querySelector('label')
314 | const secondColumn = document.querySelectorAll('.miller-columns__column')[1]
315 | const headingL2 = secondColumn.querySelector('.miller-columns__column-heading')
316 | assert.isTrue(firstLabelL1.textContent.includes(headingL2.textContent))
317 | })
318 |
319 | it('shows previous column when back link button is clicked', function() {
320 | const firstColumn = document.querySelectorAll('.miller-columns__column')[0]
321 | const firstItemL1 = firstColumn.querySelector('li')
322 | const secondColumn = document.querySelectorAll('.miller-columns__column')[1]
323 | const backButtonL2 = secondColumn.querySelector('.govuk-back-link')
324 |
325 | firstItemL1.click()
326 | assert.equal(document.querySelector('.miller-columns__column--active'), secondColumn)
327 |
328 | backButtonL2.click()
329 | assert.equal(document.querySelector('.miller-columns__column--active'), firstColumn)
330 | })
331 |
332 | it('applies aria-describedby to each individual item', function() {
333 | const millerColumnsElement = document.querySelector('miller-columns')
334 | const describedbyId = millerColumnsElement.getAttribute('aria-describedby')
335 |
336 | const millerColumnsItem = document.querySelector('.miller-columns__item')
337 |
338 | assert.equal(millerColumnsItem.getAttribute('aria-describedby'), describedbyId)
339 | })
340 |
341 | it('adds each item to the tab order', function() {
342 | const millerColumnsItem = document.querySelector('.miller-columns__item')
343 |
344 | assert.equal(millerColumnsItem.getAttribute('tabindex'), '0')
345 | })
346 |
347 | it('removes checkboxes from the focus order', function() {
348 | const item = document.querySelector('ul li')
349 | const itemCheckbox = item.querySelector('input')
350 |
351 | assert.equal(itemCheckbox.getAttribute('tabindex'), '-1')
352 | })
353 | })
354 |
355 | describe('when loading pre-selected items', function() {
356 | beforeEach(function() {
357 | const container = document.createElement('div')
358 | container.innerHTML = `
359 |
360 |
361 |
417 |
418 | `
419 | document.body.append(container)
420 | })
421 |
422 | afterEach(function() {
423 | document.body.innerHTML = undefined
424 | })
425 |
426 | it('marks items with checked inputs as selected', function() {
427 | const selectedCheckbox = document.querySelector('ul li input:checked')
428 | const listItem = selectedCheckbox.closest('li')
429 | assert.isTrue(listItem.classList.contains('miller-columns__item--active'))
430 | assert.isTrue(listItem.querySelector('input').checked)
431 | })
432 |
433 | it('marks selected item’s parent as selected', function() {
434 | const listItem = document.querySelector('ul li')
435 | assert.isTrue(listItem.classList.contains('miller-columns__item--active'))
436 | assert.isTrue(listItem.querySelector('input').checked)
437 | })
438 | })
439 | })
440 |
--------------------------------------------------------------------------------