multiselect-combo-box
Advanced tools
Comparing version 2.5.0-beta.3 to 3.0.0-alpha
import './theme/lumo/multiselect-combo-box.js'; | ||
export * from './src/multiselect-combo-box.js'; |
123
package.json
{ | ||
"description": "A multi select combo box web component based on Polymer 3 and the vaadin-combo-box", | ||
"keywords": [ | ||
"multiselect-combo-box", | ||
"web-components", | ||
"vaadin", | ||
"polymer-3", | ||
"polymer" | ||
], | ||
"name": "multiselect-combo-box", | ||
"version": "3.0.0-alpha", | ||
"description": "A multiselect combo box web component compatible with the Vaadin Web Components", | ||
"license": "Apache-2.0", | ||
"repository": { | ||
@@ -14,14 +10,19 @@ "type": "git", | ||
}, | ||
"name": "multiselect-combo-box", | ||
"version": "2.5.0-beta.3", | ||
"main": "multiselect-combo-box.js", | ||
"directories": { | ||
"test": "test" | ||
}, | ||
"author": "Goran", | ||
"license": "Apache-2.0", | ||
"bugs": { | ||
"url": "https://github.com/gatanaso/multiselect-combo-box/issues" | ||
}, | ||
"main": "multiselect-combo-box.js", | ||
"module": "multiselect-combo-box.js", | ||
"scripts": { | ||
"build": "rimraf build && rollup -c rollup.config.js", | ||
"lint": "npm-run-all --parallel lint:*", | ||
"lint:css": "stylelint src/*.js theme/**/*-styles.js", | ||
"lint:js": "eslint --ext .js,.ts *.js src test theme", | ||
"lint:types": "tsc", | ||
"start": "web-dev-server --node-resolve --open", | ||
"test": "web-test-runner test/*.test.js --node-resolve" | ||
}, | ||
"files": [ | ||
"multiselect-*.d.ts", | ||
"multiselect-*.js", | ||
@@ -31,46 +32,64 @@ "src", | ||
], | ||
"resolutions": { | ||
"inherits": "2.0.3", | ||
"samsam": "1.1.3", | ||
"supports-color": "3.1.2", | ||
"type-detect": "1.0.0" | ||
}, | ||
"husky": { | ||
"hooks": { | ||
"pre-commit": "npm run lint" | ||
} | ||
}, | ||
"scripts": { | ||
"test": "npm run lint && wct --npm", | ||
"lint": "npm-run-all --parallel lint:*", | ||
"lint:css": "stylelint --syntax html src theme demo test", | ||
"lint:html": "eslint *.html demo test --ext .html", | ||
"lint:js": "eslint *.js src", | ||
"lint:polymer": "polymer lint --rules=polymer-3 --input ./src/*.js ./theme/**/*.js", | ||
"start": "npm run lint && wct --npm && polymer serve --npm --open" | ||
}, | ||
"keywords": [ | ||
"multiselect-combo-box", | ||
"web-components", | ||
"vaadin", | ||
"polymer-3", | ||
"polymer" | ||
], | ||
"dependencies": { | ||
"@polymer/iron-resizable-behavior": "^3.0.1", | ||
"@open-wc/dedupe-mixin": "^1.3.0", | ||
"@polymer/polymer": "^3.0.0", | ||
"@vaadin/vaadin-combo-box": "^5.0.9", | ||
"@vaadin/vaadin-control-state-mixin": "^2.1.3", | ||
"@vaadin/vaadin-lumo-styles": "^1.5.0", | ||
"@vaadin/vaadin-material-styles": "^1.2.3", | ||
"@vaadin/vaadin-text-field": "^2.4.8", | ||
"@vaadin/vaadin-themable-mixin": "^1.4.4" | ||
"@vaadin/combo-box": "^22.0.0", | ||
"@vaadin/component-base": "^22.0.0", | ||
"@vaadin/field-base": "^22.0.0", | ||
"@vaadin/input-container": "^22.0.0", | ||
"@vaadin/polymer-legacy-adapter": "^22.0.2", | ||
"@vaadin/vaadin-lumo-styles": "^22.0.0", | ||
"@vaadin/vaadin-material-styles": "^22.0.0", | ||
"@vaadin/vaadin-themable-mixin": "^22.0.0" | ||
}, | ||
"devDependencies": { | ||
"@esm-bundle/chai": "^4.3.4", | ||
"@polymer/iron-component-page": "^4.0.1", | ||
"@polymer/iron-demo-helpers": "^3.0.0", | ||
"@webcomponents/webcomponentsjs": "^2.0.0", | ||
"eslint": "^6.0.1", | ||
"eslint-config-vaadin": "^0.2.7", | ||
"eslint-plugin-html": "^6.0.0", | ||
"husky": "^3.0.8", | ||
"@rollup/plugin-node-resolve": "^11.2.1", | ||
"@typescript-eslint/eslint-plugin": "^5.4.0", | ||
"@typescript-eslint/parser": "^5.4.0", | ||
"@vaadin/testing-helpers": "^0.3.2", | ||
"@web/dev-server": "^0.1.28", | ||
"@web/rollup-plugin-html": "^1.10.1", | ||
"@web/test-runner": "^0.13.22", | ||
"@web/test-runner-commands": "^0.6.0", | ||
"@web/test-runner-playwright": "^0.8.8", | ||
"eslint": "^8.3.0", | ||
"eslint-config-prettier": "^8.3.0", | ||
"eslint-plugin-prettier": "^4.0.0", | ||
"eslint-plugin-simple-import-sort": "7.0.0", | ||
"husky": "^7.0.4", | ||
"lint-staged": "^12.1.2", | ||
"npm-run-all": "^4.1.5", | ||
"stylelint": "^10.1.0", | ||
"stylelint-config-vaadin": "^0.1.4", | ||
"wct-browser-legacy": "^1.0.2", | ||
"wct-istanbul": "^0.14.3", | ||
"web-component-tester": "^6.9.2" | ||
"postcss-lit": "^0.2.0", | ||
"prettier": "^2.4.1", | ||
"prettier-plugin-package": "^1.3.0", | ||
"replace-in-file": "^6.3.2", | ||
"rimraf": "^3.0.2", | ||
"rollup": "^2.66.1", | ||
"rollup-plugin-terser": "^7.0.2", | ||
"simple-git-hooks": "^2.7.0", | ||
"sinon": "^12.0.1", | ||
"stylelint": "^14.1.0", | ||
"stylelint-config-prettier": "^9.0.3", | ||
"stylelint-config-vaadin": "^0.3.0", | ||
"typescript": "^4.5.2" | ||
}, | ||
"lint-staged": { | ||
"*.{js,ts}": [ | ||
"eslint --fix", | ||
"prettier --write" | ||
] | ||
}, | ||
"simple-git-hooks": { | ||
"pre-commit": "npx lint-staged" | ||
} | ||
} |
@@ -1,42 +0,30 @@ | ||
[![Published on webcomponents.org](https://img.shields.io/badge/webcomponents.org-published-blue.svg)](https://www.webcomponents.org/element/multiselect-combo-box) | ||
[![Published on Vaadin Directory](https://img.shields.io/badge/Vaadin%20Directory-published-00b4f0.svg)](https://vaadin.com/directory/component/gatanasomultiselect-combo-box) | ||
[![Build Status](https://travis-ci.org/gatanaso/multiselect-combo-box.svg?branch=master)](https://travis-ci.org/gatanaso/multiselect-combo-box) | ||
[![codecov](https://codecov.io/gh/gatanaso/multiselect-combo-box/branch/master/graph/badge.svg)](https://codecov.io/gh/gatanaso/multiselect-combo-box) | ||
[![Stars on vaadin.com/directory](https://img.shields.io/vaadin-directory/star/gatanasomultiselect-combo-box.svg)](https://vaadin.com/directory/component/gatanasomultiselect-combo-box) | ||
[![Tests](https://github.com/gatanaso/multiselect-combo-box/actions/workflows/tests.yml/badge.svg)](https://github.com/gatanaso/multiselect-combo-box/actions/workflows/tests.yml) | ||
# \<multiselect-combo-box\> | ||
A multiselect combo box web component based on [Polymer](https://github.com/Polymer/polymer) and the [`vaadin-combo-box`](https://github.com/vaadin/vaadin-combo-box). | ||
A multiselect combo box web component compatible with the [Vaadin Web Components](https://github.com/vaadin/web-components) | ||
*Compatible with the core set of [Vaadin Components](https://github.com/vaadin/vaadin-core).* | ||
#### [Demo ↗](https://multiselect-combo-box.web.app) | [Material Theme Demo ↗](https://multiselect-combo-box-material.web.app) | ||
#### [Live Demo ↗](https://multiselect-combo-box.web.app/demo/) | [Material Theme Live Demo ↗](https://multiselect-combo-box-material.web.app/demo/material) | ||
## Getting started | ||
## Getting started | ||
### Polymer 3 (npm) | ||
Install the `multiselect-combo-box`: | ||
``` | ||
npm install multiselect-combo-box --save | ||
``` | ||
Once installed, import in your applicaiton: | ||
``` | ||
import 'multiselect-combo-box/multiselect-combo-box.js'; | ||
``` | ||
Add to your page: | ||
``` | ||
<multiselect-combo-box label="Select items"></multiselect-combo-box> | ||
``` | ||
### Polymer 2.x (bower) | ||
Install the `multiselect-combo-box`: | ||
``` | ||
bower install --save gatanaso/multiselect-combo-box#1.1.0 | ||
``` | ||
Once installed, import in your applicaiton: | ||
``` | ||
<link rel="import" href="bower_components/multiselect-combo-box/multiselect-combo-box.html"> | ||
``` | ||
Add to your page: | ||
``` | ||
<multiselect-combo-box label="Select items"></multiselect-combo-box> | ||
``` | ||
@@ -48,8 +36,9 @@ > For more usage examples, see the [demo](https://multiselect-combo-box.firebaseapp.com/demo/). | ||
1. Fork the `multiselect-combo-box` repository and clone it locally. | ||
1. Make sure you have the [Polymer CLI](https://www.npmjs.com/package/polymer-cli) and npm (packaged with [Node.js](https://nodejs.org)) installed locally. | ||
1. Make sure you have npm (packaged with [Node.js](https://nodejs.org)) installed locally. | ||
1. To install the element's dependencies, in the `multiselect-combo-box` directory, run: `npm install` | ||
* if using the Polymer 2.x version, also run: `bower install` | ||
### Running the project locally | ||
To perform linting, run the tests and automatically open the demo page, run: | ||
``` | ||
@@ -61,6 +50,7 @@ npm run start | ||
* `master` the latest (Polymer 3) version of the `multiselect-combo-box` | ||
* `polymer-2` the Polymer 2.x version of the `multiselect-combo-box` | ||
- `master` the latest version of the `multiselect-combo-box` | ||
- `polymer-2` the Polymer 2.x version of the `multiselect-combo-box` which is not maintained anymore. | ||
## Java API | ||
The Vaadin Flow Java compatible version of this component is available on the [Vaadin Directory](https://vaadin.com/directory/component/multiselect-combo-box) and [GitHub](https://github.com/gatanaso/multiselect-combo-box-flow). |
/** | ||
* @polymerMixin | ||
*/ | ||
export const MultiselectComboBoxMixin = (base) => class extends base { | ||
export const MultiselectComboBoxMixin = (base) => | ||
class extends base { | ||
static get properties() { | ||
return { | ||
/** | ||
* A full set of items to filter the visible options from. | ||
* The items can be of either `String` or `Object` type. | ||
*/ | ||
items: { | ||
type: Array | ||
}, | ||
static get properties() { | ||
return { | ||
/** | ||
* The list of items. | ||
*/ | ||
items: Array, | ||
/** | ||
* The item property used for a visual representation of the item. | ||
* @attr {string} item-label-path | ||
*/ | ||
itemLabelPath: { | ||
type: String | ||
} | ||
}; | ||
} | ||
/** | ||
* The input placeholder. | ||
*/ | ||
placeholder: { | ||
type: String, | ||
value: '' | ||
}, | ||
/** | ||
* This attribute indicates that the component has a value. | ||
*/ | ||
hasValue: { | ||
type: Boolean, | ||
value: false, | ||
reflectToAttribute: true | ||
}, | ||
/** | ||
* This attribute indicates that the component has a label. | ||
*/ | ||
hasLabel: { | ||
type: Boolean, | ||
value: false, | ||
reflectToAttribute: true | ||
}, | ||
/** | ||
* This attribute indicates that the component is rendered in 'compact mode'. | ||
* In this mode, the component displays the number of items currently selected. | ||
*/ | ||
compactMode: { | ||
type: Boolean, | ||
value: false, | ||
reflectToAttribute: true | ||
}, | ||
/** | ||
* Custom function for generating the display label when in compact mode. | ||
* | ||
* This function receives the array of selected items and should return | ||
* a string value that will be used as the display label. | ||
*/ | ||
compactModeLabelGenerator: Function, | ||
/** | ||
* The item property to be used as the `label` in combo-box. | ||
*/ | ||
itemLabelPath: String, | ||
/** | ||
* The item property to be used as the `value` of combo-box. | ||
*/ | ||
itemValuePath: String, | ||
/** | ||
* Path for the id of the item. If `items` is an array of objects, | ||
* the `itemIdPath` is used to compare and identify the same item | ||
* in `selectedItem`. | ||
*/ | ||
itemIdPath: String, | ||
/** | ||
* The theme name attribute. | ||
* Used to communicate theme information to | ||
* component internals (currently used for the material theme). | ||
*/ | ||
theme: String, | ||
/** | ||
* Set to true to disable this element. | ||
*/ | ||
disabled: { | ||
type: Boolean, | ||
value: false, | ||
reflectToAttribute: true | ||
}, | ||
/** | ||
* Set to true to display the clear icon which clears the input. | ||
*/ | ||
clearButtonVisible: { | ||
type: Boolean, | ||
value: false | ||
} | ||
}; | ||
} | ||
/** | ||
* Returns the item display label. | ||
* @protected | ||
*/ | ||
_getItemLabel(item, itemLabelPath) { | ||
return item && item.hasOwnProperty(itemLabelPath) ? item[itemLabelPath] : item; | ||
} | ||
/** | ||
* Retrieves the component display label when in compact mode. | ||
* @protected | ||
*/ | ||
_getCompactModeLabel(items) { | ||
if (this.compactModeLabelGenerator && typeof this.compactModeLabelGenerator === 'function') { | ||
return this.compactModeLabelGenerator(items); | ||
} else { | ||
const suffix = (items.length === 0 || items.length > 1) ? 'values' : 'value'; | ||
return `${items.length} ${suffix}`; | ||
/** | ||
* Returns the item display label. | ||
* @protected | ||
*/ | ||
_getItemLabel(item, itemLabelPath) { | ||
return item && Object.prototype.hasOwnProperty.call(item, itemLabelPath) ? item[itemLabelPath] : item; | ||
} | ||
} | ||
}; | ||
}; |
@@ -1,516 +0,644 @@ | ||
import {PolymerElement} from '@polymer/polymer/polymer-element.js'; | ||
import {html} from '@polymer/polymer/lib/utils/html-tag.js'; | ||
import {ControlStateMixin} from '@vaadin/vaadin-control-state-mixin/vaadin-control-state-mixin.js'; | ||
import {ThemableMixin} from '@vaadin/vaadin-themable-mixin/vaadin-themable-mixin.js'; | ||
import {ThemePropertyMixin} from '@vaadin/vaadin-themable-mixin/vaadin-theme-property-mixin.js'; | ||
import {ComboBoxPlaceholder} from '@vaadin/vaadin-combo-box/src/vaadin-combo-box-placeholder.js'; | ||
import {FlattenedNodesObserver} from '@polymer/polymer/lib/utils/flattened-nodes-observer.js'; | ||
import {mixinBehaviors} from '@polymer/polymer/lib/legacy/class.js'; | ||
import {IronResizableBehavior} from '@polymer/iron-resizable-behavior/iron-resizable-behavior.js'; | ||
import {MultiselectComboBoxMixin} from './multiselect-combo-box-mixin.js'; | ||
import '@polymer/polymer/lib/elements/dom-repeat.js'; | ||
import '@vaadin/polymer-legacy-adapter/template-renderer.js'; | ||
import './multiselect-combo-box-chip.js'; | ||
import './multiselect-combo-box-container.js'; | ||
import './multiselect-combo-box-internal.js'; | ||
import { html, PolymerElement } from '@polymer/polymer/polymer-element.js'; | ||
import { DirMixin } from '@vaadin/component-base/src/dir-mixin.js'; | ||
import { processTemplates } from '@vaadin/component-base/src/templates.js'; | ||
import { InputControlMixin } from '@vaadin/field-base/src/input-control-mixin.js'; | ||
import { InputController } from '@vaadin/field-base/src/input-controller.js'; | ||
import { LabelledInputController } from '@vaadin/field-base/src/labelled-input-controller.js'; | ||
import { inputFieldShared } from '@vaadin/field-base/src/styles/input-field-shared-styles.js'; | ||
import { css, registerStyles, ThemableMixin } from '@vaadin/vaadin-themable-mixin/vaadin-themable-mixin.js'; | ||
import { MultiselectComboBoxMixin } from './multiselect-combo-box-mixin.js'; | ||
import '@vaadin/vaadin-combo-box/src/vaadin-combo-box-light.js'; | ||
import './multiselect-combo-box-input.js'; | ||
const multiselectComboBox = css` | ||
[hidden] { | ||
display: none !important; | ||
} | ||
{ | ||
/** | ||
* `multiselect-combo-box` | ||
* | ||
* Multi select combo box based on the vaadin-combo-box | ||
* | ||
* @customElement | ||
* @polymer | ||
* @demo demo/index.html | ||
* @appliesMixin MultiselectComboBoxMixin | ||
*/ | ||
class MultiselectComboBox extends | ||
ControlStateMixin( | ||
ThemePropertyMixin( | ||
ThemableMixin( | ||
MultiselectComboBoxMixin(mixinBehaviors([IronResizableBehavior], PolymerElement))))) { | ||
:host([has-value]) ::slotted(input:placeholder-shown) { | ||
color: transparent !important; | ||
} | ||
static get template() { | ||
return html` | ||
<style> | ||
:host { | ||
display: inline-flex; | ||
} | ||
:host([has-value]:not([readonly])) [class$='container'] { | ||
width: auto; | ||
} | ||
:host([hidden]) { | ||
display: none !important; | ||
} | ||
[part='compact-mode-prefix'] { | ||
display: flex; | ||
align-items: center; | ||
} | ||
:host::before { | ||
content: "\\2003"; | ||
width: 0; | ||
box-sizing: border-box; | ||
display: inline-flex; | ||
align-items: center; | ||
} | ||
::slotted(input) { | ||
flex-basis: 80px; | ||
} | ||
`; | ||
.multiselect-combo-box-container { | ||
display: flex; | ||
flex-direction: column; | ||
min-width: 100%; | ||
max-width: 100%; | ||
} | ||
registerStyles('multiselect-combo-box', [inputFieldShared, multiselectComboBox], { | ||
moduleId: 'multiselect-combo-box-styles' | ||
}); | ||
[part="label"]:empty { | ||
display: none; | ||
} | ||
/** | ||
* `<multiselect-combo-box>` is a web component that wraps `<vaadin-combo-box>` and extends | ||
* its functionality to allow selecting multiple items, in addition to basic features. | ||
* | ||
* ```html | ||
* <multiselect-combo-box id="comboBox"></multiselect-combo-box> | ||
* ``` | ||
* | ||
* ```js | ||
* const comboBox = document.querySelector('#comboBox'); | ||
* comboBox.items = ['apple', 'banana', 'lemon', 'orange']; | ||
* comboBox.selectedItems = ['lemon', 'orange']; | ||
* ``` | ||
* | ||
* ### Styling | ||
* | ||
* The following shadow DOM parts are available for styling: | ||
* | ||
* Part name | Description | ||
* -----------------------|---------------- | ||
* `chip` | Chip shown for every selected item in default mode | ||
* `compact-mode-prefix` | The selected items counter shown in compact mode | ||
* `label` | The label element | ||
* `input-field` | The element that wraps prefix, value and suffix | ||
* `clear-button` | The clear button | ||
* `error-message` | The error message element | ||
* `helper-text` | The helper text element wrapper | ||
* `required-indicator` | The `required` state indicator element | ||
* `toggle-button` | The toggle button | ||
* | ||
* The following state attributes are available for styling: | ||
* | ||
* Attribute | Description | ||
* -----------------------|----------------- | ||
* `compact-mode` | Set when the element uses compact mode | ||
* `disabled` | Set to a disabled element | ||
* `has-value` | Set when the element has a value | ||
* `has-label` | Set when the element has a label | ||
* `has-helper` | Set when the element has helper text or slot | ||
* `has-error-message` | Set when the element has an error message | ||
* `invalid` | Set when the element is invalid | ||
* `focused` | Set when the element is focused | ||
* `focus-ring` | Set when the element is keyboard focused | ||
* `opened` | Set when the dropdown is open | ||
* `ordered` | Set when the element uses ordered mode | ||
* `readonly` | Set to a readonly element | ||
* | ||
* ### Internal components | ||
* | ||
* In addition to `<multiselect-combo-box>` itself, the following internal | ||
* components are themable: | ||
* | ||
* - `<multiselect-combo-box-overlay>` - has the same API as `<vaadin-overlay>`. | ||
* - `<multiselect-combo-box-item>` - has the same API as `<vaadin-item>`. | ||
* - `<multiselect-combo-box-container>` - has the same API as `<vaadin-input-container>`. | ||
* | ||
* Note: the `theme` attribute value set on `<multiselect-combo-box>` is | ||
* propagated to these components. | ||
* | ||
* See [Styling Components](https://vaadin.com/docs/latest/ds/customization/styling-components) documentation. | ||
* | ||
* @fires {Event} change - Fired when the user commits a value change. | ||
* @fires {CustomEvent} custom-values-set - Fired when the user sets a custom value. | ||
* @fires {CustomEvent} filter-value-changed - Fired when the `filterValue` property changes. | ||
* @fires {CustomEvent} invalid-changed - Fired when the `invalid` property changes. | ||
* @fires {CustomEvent} selected-items-changed - Fired when the `selectedItems` property changes. | ||
* | ||
* @extends HTMLElement | ||
* @mixes DirMixin | ||
* @mixes ThemableMixin | ||
* @mixes InputControlMixin | ||
* @mixes MultiselectComboBoxMixin | ||
*/ | ||
class MultiselectComboBox extends MultiselectComboBoxMixin(InputControlMixin(ThemableMixin(DirMixin(PolymerElement)))) { | ||
static get is() { | ||
return 'multiselect-combo-box'; | ||
} | ||
[part="combo-box"] { | ||
display: block; | ||
width: 100%; | ||
min-width: 0; | ||
} | ||
static get template() { | ||
return html` | ||
<div class="multiselect-combo-box-container"> | ||
<div part="label"> | ||
<slot name="label"></slot> | ||
<span part="required-indicator" aria-hidden="true" on-click="focus"></span> | ||
</div> | ||
[part="combo-box"][hidden] { | ||
display: none; | ||
} | ||
<multiselect-combo-box-internal | ||
id="comboBox" | ||
items="[[items]]" | ||
item-id-path="[[itemIdPath]]" | ||
item-label-path="[[itemLabelPath]]" | ||
item-value-path="[[itemValuePath]]" | ||
disabled="[[disabled]]" | ||
readonly="[[readonly]]" | ||
auto-open-disabled="[[autoOpenDisabled]]" | ||
allow-custom-value="[[allowCustomValues]]" | ||
data-provider="[[dataProvider]]" | ||
filter="{{filterValue}}" | ||
filtered-items="[[filteredItems]]" | ||
opened="{{opened}}" | ||
renderer="[[renderer]]" | ||
theme$="[[theme]]" | ||
suppress-template-warning | ||
on-combo-box-item-selected="_onComboBoxItemSelected" | ||
on-change="_onComboBoxChange" | ||
on-custom-value-set="_onCustomValueSet" | ||
> | ||
<multiselect-combo-box-container | ||
part="input-field" | ||
readonly="[[readonly]]" | ||
disabled="[[disabled]]" | ||
invalid="[[invalid]]" | ||
theme$="[[theme]]" | ||
> | ||
<div | ||
part="compact-mode-prefix" | ||
hidden$="[[_isCompactModeHidden(readonly, compactMode, _hasValue)]]" | ||
slot="prefix" | ||
> | ||
[[_getCompactModeLabel(selectedItems, compactModeLabelGenerator)]] | ||
</div> | ||
<template id="repeat" is="dom-repeat" items="[[selectedItems]]" slot="prefix"> | ||
<multiselect-combo-box-chip | ||
slot="prefix" | ||
part="chip" | ||
item="[[item]]" | ||
label="[[_getItemLabel(item, itemLabelPath)]]" | ||
hidden$="[[_isTokensHidden(readonly, compactMode, _hasValue)]]" | ||
on-item-removed="_onItemRemoved" | ||
on-mousedown="_preventBlur" | ||
></multiselect-combo-box-chip> | ||
</template> | ||
<slot name="input"></slot> | ||
<div id="clearButton" part="clear-button" slot="suffix"></div> | ||
<div id="toggleButton" class="toggle-button" part="toggle-button" slot="suffix"></div> | ||
</multiselect-combo-box-container> | ||
</multiselect-combo-box-internal> | ||
[part="input-field"] { | ||
width: 100%; | ||
min-width: 0; | ||
position: relative; | ||
} | ||
<div part="helper-text"> | ||
<slot name="helper"></slot> | ||
</div> | ||
:host([disabled]) [part="label"] { | ||
pointer-events: none; | ||
} | ||
<div part="error-message"> | ||
<slot name="error-message"></slot> | ||
</div> | ||
</div> | ||
`; | ||
} | ||
</style> | ||
static get properties() { | ||
return { | ||
/** | ||
* Set true to prevent the overlay from opening automatically. | ||
* @attr {boolean} auto-open-disabled | ||
*/ | ||
autoOpenDisabled: Boolean, | ||
<div class="multiselect-combo-box-container"> | ||
/** | ||
* When true, the component does not render chips for every selected value. | ||
* Instead, only the number of currently selected items is shown. | ||
* @attr {boolean} compact-mode | ||
*/ | ||
compactMode: { | ||
type: Boolean, | ||
reflectToAttribute: true | ||
}, | ||
<label part="label">[[label]]</label> | ||
/** | ||
* Custom function for generating the display label when in compact mode. | ||
* | ||
* This function receives the array of selected items and should return | ||
* a string value that will be used as the display label. | ||
*/ | ||
compactModeLabelGenerator: { | ||
type: Object | ||
}, | ||
<div part="readonly-container" hidden\$="[[!readonly]]"> | ||
[[_getReadonlyValue(selectedItems, itemLabelPath, compactMode, readonlyValueSeparator)]] | ||
</div> | ||
/** | ||
* Path for the value of the item. If `items` is an array of objects, | ||
* this property is used as a string value for the selected item. | ||
* @attr {string} item-value-path | ||
*/ | ||
itemValuePath: String, | ||
<vaadin-combo-box-light | ||
id="comboBox" | ||
part="combo-box" | ||
hidden\$="[[readonly]]" | ||
items="[[items]]" | ||
item-id-path="[[itemIdPath]]" | ||
item-label-path="[[itemLabelPath]]" | ||
item-value-path="[[itemValuePath]]" | ||
on-change="_comboBoxValueChanged" | ||
disabled="[[disabled]]" | ||
page-size="[[pageSize]]" | ||
filter="{{filterValue}}" | ||
filtered-items="[[filteredItems]]" | ||
allow-custom-value="[[allowCustomValues]]" | ||
on-custom-value-set="_handleCustomValueSet"> | ||
/** | ||
* Path for the id of the item, used to detect whether the item is selected. | ||
* @attr {string} item-id-path | ||
*/ | ||
itemIdPath: { | ||
type: String | ||
}, | ||
<multiselect-combo-box-input | ||
id="input" | ||
class="input" | ||
part="input-field" | ||
placeholder="[[placeholder]]" | ||
item-label-path="[[itemLabelPath]]" | ||
items="[[selectedItems]]" | ||
compact-mode="[[compactMode]]" | ||
compact-mode-label-generator="[[compactModeLabelGenerator]]" | ||
on-item-removed="_handleItemRemoved" | ||
on-remove-all-items="_handleRemoveAllItems" | ||
has-value="[[hasValue]]" | ||
has-label="[[hasLabel]]" | ||
theme\$="[[theme]]" | ||
disabled="[[disabled]]" | ||
clear-button-visible="[[clearButtonVisible]]"> | ||
</multiselect-combo-box-input> | ||
</vaadin-combo-box-light> | ||
/** | ||
* The list of selected items. | ||
* Note: modifying the selected items creates a new array each time. | ||
*/ | ||
selectedItems: { | ||
type: Array, | ||
value: () => [], | ||
notify: true | ||
}, | ||
<div part="error-message" hidden\$="[[!invalid]]">[[errorMessage]]</div> | ||
/** | ||
* True if the dropdown is open, false otherwise. | ||
*/ | ||
opened: { | ||
type: Boolean, | ||
notify: true, | ||
value: false, | ||
reflectToAttribute: true | ||
}, | ||
</div> | ||
`; | ||
} | ||
/** | ||
* When true, the list of selected items is kept ordered in ascending lexical order. | ||
* | ||
* When `itemLabelPath` is specified, corresponding property is used for ordering. | ||
* Otherwise the items themselves are compared using `localCompare`. | ||
*/ | ||
ordered: { | ||
type: Boolean, | ||
value: false, | ||
reflectToAttribute: true | ||
}, | ||
static get is() { | ||
return 'multiselect-combo-box'; | ||
} | ||
/** | ||
* Number of items fetched at a time from the data provider. | ||
* @attr {number} page-size | ||
*/ | ||
pageSize: { | ||
type: Number, | ||
value: 50, | ||
observer: '_pageSizeChanged' | ||
}, | ||
constructor() { | ||
super(); | ||
this._boundCustomOverlaySelectedItemChanged = this._customOverlaySelectedItemChanged.bind(this); | ||
} | ||
/** | ||
* Function that provides items lazily. Receives two arguments: | ||
* | ||
* - `params` - Object with the following properties: | ||
* - `params.page` Requested page index | ||
* - `params.pageSize` Current page size | ||
* - `params.filter` Currently applied filter | ||
* | ||
* - `callback(items, size)` - Callback function with arguments: | ||
* - `items` Current page of items | ||
* - `size` Total number of items. | ||
*/ | ||
dataProvider: { | ||
type: Object, | ||
observer: '_dataProviderChanged' | ||
}, | ||
ready() { | ||
super.ready(); | ||
/** | ||
* The join separator used for the 'display value' when in read-only mode. | ||
* @attr {string} readonly-value-separator | ||
*/ | ||
readonlyValueSeparator: { | ||
type: String, | ||
value: ', ' | ||
}, | ||
// replace listener to modify default behavior | ||
this.$.comboBox.$.overlay.removeEventListener('selection-changed', this.$.comboBox._boundOverlaySelectedItemChanged); | ||
this.$.comboBox.$.overlay.addEventListener('selection-changed', this._boundCustomOverlaySelectedItemChanged); | ||
/** | ||
* When true, the user can input a value that is not present in the items list. | ||
* @attr {boolean} allow-custom-values | ||
*/ | ||
allowCustomValues: { | ||
type: Boolean, | ||
value: false | ||
}, | ||
// modify check to allow custom renderers | ||
this.$.comboBox.$.overlay._isItemSelected = this._customIsSelected.bind(this); | ||
/** | ||
* Custom function for rendering the content of every item. | ||
* Receives three arguments: | ||
* | ||
* - `root` The `<multiselect-combo-box-item>` internal container DOM element. | ||
* - `comboBox` The reference to the underlying `<vaadin-combo-box>` element. | ||
* - `model` The object with the properties related with the rendered | ||
* item, contains: | ||
* - `model.index` The index of the rendered item. | ||
* - `model.item` The item. | ||
*/ | ||
renderer: Function, | ||
this._observer = new FlattenedNodesObserver(this, (info) => { | ||
this._setTemplateFromNodes(info.addedNodes); | ||
}); | ||
/** | ||
* Filtering string the user has typed into the input field. | ||
* @attr {string} filter-value | ||
*/ | ||
filterValue: { | ||
type: String, | ||
value: '', | ||
notify: true | ||
}, | ||
this._notifyReady(); // only relevant when used with Vaadin Flow | ||
} | ||
/** | ||
* A subset of items, filtered based on the user input. Filtered items | ||
* can be assigned directly to omit the internal filtering functionality. | ||
* The items can be of either `String` or `Object` type. | ||
*/ | ||
filteredItems: Array, | ||
static get properties() { | ||
return { | ||
/** | ||
* The component label. | ||
*/ | ||
label: { | ||
type: String, | ||
value: '', | ||
observer: '_labelChanged' | ||
}, | ||
/** @protected */ | ||
_hasValue: { | ||
type: Boolean, | ||
value: false | ||
} | ||
}; | ||
} | ||
/** | ||
* This attribute indicates that the component has a label. | ||
*/ | ||
hasLabel: { | ||
type: Boolean, | ||
value: false, | ||
reflectToAttribute: true | ||
}, | ||
static get observers() { | ||
return [ | ||
'_selectedItemsChanged(selectedItems, selectedItems.*)', | ||
'_updateReadOnlyMode(inputElement, readonly, itemLabelPath, compactMode, readonlyValueSeparator, selectedItems, selectedItems.*)', | ||
'_updateItems(ordered, compactMode, itemLabelPath, selectedItems, selectedItems.*)' | ||
]; | ||
} | ||
/** | ||
* The title attribute. | ||
*/ | ||
title: { | ||
type: String, | ||
value: '', | ||
reflectToAttribute: true | ||
}, | ||
/** | ||
* Used by `ClearButtonMixin` as a reference to the clear button element. | ||
* @protected | ||
* @return {!HTMLElement} | ||
*/ | ||
get clearElement() { | ||
return this.$.clearButton; | ||
} | ||
/** | ||
* The list of selected items. | ||
* | ||
* Note: modifying the selected items creates a new array each time. | ||
*/ | ||
selectedItems: { | ||
type: Array, | ||
value: () => [], | ||
notify: true | ||
}, | ||
/** @protected */ | ||
ready() { | ||
super.ready(); | ||
/** | ||
* This attribute specifies if the list of selected items should be kept ordered in ascending lexical order. | ||
* | ||
* If the `itemLabelPath` is specified, that value is used for ordering, otherwise the items themselves are | ||
* compared using `localCompare`. | ||
*/ | ||
ordered: { | ||
type: Boolean, | ||
value: false, | ||
reflectToAttribute: true | ||
}, | ||
this.addController( | ||
new InputController(this, (input) => { | ||
this._setInputElement(input); | ||
this._setFocusElement(input); | ||
this.stateTarget = input; | ||
this.ariaTarget = input; | ||
}) | ||
); | ||
this.addController(new LabelledInputController(this.inputElement, this._labelController)); | ||
/** | ||
* Number of items fetched at a time from the dataprovider. | ||
* | ||
* This property is delegated to the underlying `vaadin-combo-box`. | ||
*/ | ||
pageSize: { | ||
type: Number, | ||
value: 50, | ||
observer: '_pageSizeObserver' | ||
}, | ||
processTemplates(this); | ||
} | ||
/** | ||
* The `readonly` attribute. | ||
*/ | ||
readonly: { | ||
type: Boolean, | ||
value: false, | ||
reflectToAttribute: true | ||
}, | ||
/** | ||
* Returns true if the current input value satisfies all constraints (if any). | ||
* @return {boolean} | ||
*/ | ||
checkValidity() { | ||
return this.required ? this._hasValue : true; | ||
} | ||
/** | ||
* The `required` attribute. | ||
*/ | ||
required: { | ||
type: Boolean, | ||
value: false, | ||
reflectToAttribute: true | ||
}, | ||
/** | ||
* Override method inherited from `InputMixin` to forward the input to combo-box. | ||
* @protected | ||
* @override | ||
*/ | ||
_inputElementChanged(input) { | ||
super._inputElementChanged(input); | ||
/** | ||
* The `invalid` attribute. | ||
*/ | ||
invalid: { | ||
type: Boolean, | ||
value: false, | ||
reflectToAttribute: true, | ||
notify: true | ||
}, | ||
if (input) { | ||
this.$.comboBox._setInputElement(input); | ||
} | ||
} | ||
/** | ||
* The `invalid` state error-message. | ||
*/ | ||
errorMessage: String, | ||
/** | ||
* Override method inherited from `InputMixin` | ||
* to keep attribute after clearing the input. | ||
* @protected | ||
* @override | ||
*/ | ||
_toggleHasValue() { | ||
super._toggleHasValue(this._hasValue); | ||
} | ||
/** | ||
* The join separator used for the 'display value' when in read-only mode. | ||
*/ | ||
readonlyValueSeparator: { | ||
type: String, | ||
value: ', ' // default value | ||
}, | ||
/** | ||
* Override method inherited from `FocusMixin` to validate on blur. | ||
* @param {boolean} focused | ||
* @protected | ||
*/ | ||
_setFocused(focused) { | ||
super._setFocused(focused); | ||
/** | ||
* If `true`, the user can input a value that is not present in the items list. | ||
* `value` property will be set to the input value in this case. | ||
* | ||
* This property is delegated to the underlying `vaadin-combo-box`. | ||
*/ | ||
allowCustomValues: { | ||
type: Boolean, | ||
value: false | ||
}, | ||
if (!focused) { | ||
this.validate(); | ||
} | ||
} | ||
/** | ||
* Custom function for rendering the content of every item. | ||
* Receives three arguments: | ||
* | ||
* - `root` The `<vaadin-combo-box-item>` internal container DOM element. | ||
* - `comboBox` The reference to the `<vaadin-combo-box>` element. | ||
* - `model` The object with the properties related with the rendered | ||
* item, contains: | ||
* - `model.index` The index of the rendered item. | ||
* - `model.item` The item. | ||
*/ | ||
renderer: Function, | ||
/** @private */ | ||
_isCompactModeHidden(readonly, compactMode, hasValue) { | ||
return readonly || !compactMode || !hasValue; | ||
} | ||
_itemTemplate: Object, | ||
/** @private */ | ||
_isTokensHidden(readonly, compactMode, hasValue) { | ||
return readonly || compactMode || !hasValue; | ||
} | ||
/** | ||
* Filtering string the user has typed into the input field. | ||
*/ | ||
filterValue: { | ||
type: String, | ||
value: '', | ||
notify: true | ||
}, | ||
/** @private */ | ||
_updateItems(ordered, compactMode, itemLabelPath, selectedItems) { | ||
// Set title when in compact mode to indicate which items are selected. | ||
this.title = compactMode ? this._getDisplayValue(selectedItems, itemLabelPath, ', ') : undefined; | ||
/** | ||
* A subset of items, filtered based on the user input. Filtered items | ||
* can be assigned directly to omit the internal filtering functionality. | ||
* The items can be of either `String` or `Object` type. | ||
*/ | ||
filteredItems: Array | ||
}; | ||
if (ordered && !compactMode) { | ||
this._sortSelectedItems(selectedItems); | ||
} | ||
} | ||
static get observers() { | ||
return [ | ||
'_selectedItemsObserver(selectedItems, selectedItems.*)', | ||
'_templateOrRendererChanged(_itemTemplate, renderer)', | ||
'_observeOffsetHeight(errorMessage, invalid, label)' | ||
]; | ||
/** @private */ | ||
_updateReadOnlyMode(inputElement, readonly, itemLabelPath, compactMode, separator, selectedItems) { | ||
if (inputElement) { | ||
inputElement.value = readonly ? this._getReadonlyValue(selectedItems, itemLabelPath, compactMode, separator) : ''; | ||
} | ||
} | ||
/** | ||
* Validates the component value. | ||
* | ||
* This method will set the components `valid` and `invalid` properties accordingly. | ||
*/ | ||
validate() { | ||
const valid = this.required ? this.hasValue : true; | ||
this.invalid = !valid; | ||
return valid; | ||
/** @private */ | ||
_pageSizeChanged(pageSize, oldPageSize) { | ||
if (Math.floor(pageSize) !== pageSize || pageSize <= 0) { | ||
this.pageSize = oldPageSize; | ||
console.error('"pageSize" value must be an integer > 0'); | ||
} | ||
_selectedItemsObserver(selectedItems) { | ||
this.hasValue = selectedItems && selectedItems.length > 0; | ||
this.$.comboBox.pageSize = this.pageSize; | ||
} | ||
if (this.ordered && !this.compactMode) { | ||
this._sortSelectedItems(selectedItems); | ||
} | ||
/** @private */ | ||
_selectedItemsChanged(selectedItems) { | ||
this._hasValue = Boolean(selectedItems && selectedItems.length); | ||
this.compactMode && (this.title = this._getDisplayValue(selectedItems, this.itemLabelPath, ', ')); | ||
this._toggleHasValue(); | ||
// manually force a render | ||
this.$.comboBox.$.overlay._selectedItem = {}; | ||
// Re-render chips | ||
this.__updateChips(); | ||
setTimeout(() => this._notifyResizeIfNeeded(), 0); | ||
} | ||
// Re-render scroller | ||
this.$.comboBox.$.dropdown._scroller.__virtualizer.update(); | ||
_templateOrRendererChanged(template, renderer) { | ||
this.$.comboBox._itemTemplate = template; | ||
this.$.comboBox.renderer = renderer; | ||
} | ||
// Wait for chips to render | ||
requestAnimationFrame(() => { | ||
this.$.comboBox.$.dropdown._setOverlayWidth(); | ||
}); | ||
} | ||
_observeOffsetHeight() { | ||
this._notifyResizeIfNeeded(); | ||
/** @private */ | ||
_getCompactModeLabel(items) { | ||
if (typeof this.compactModeLabelGenerator === 'function') { | ||
return this.compactModeLabelGenerator(items); | ||
} | ||
_dispatchChangeEvent() { | ||
this.dispatchEvent(new CustomEvent('change', {bubbles: true})); | ||
} | ||
const suffix = items.length === 0 || items.length > 1 ? 'values' : 'value'; | ||
return `${items.length} ${suffix}`; | ||
} | ||
_comboBoxValueChanged(event, selectedItem) { | ||
const item = selectedItem || this.$.comboBox.selectedItem; | ||
if (!item) { | ||
return; | ||
} | ||
/** @private */ | ||
_getReadonlyValue(selectedItems, itemLabelPath, compactMode, readonlyValueSeparator) { | ||
return compactMode | ||
? this._getCompactModeLabel(selectedItems) | ||
: this._getDisplayValue(selectedItems, itemLabelPath, readonlyValueSeparator); | ||
} | ||
const update = this.selectedItems.slice(0); | ||
const index = this._findIndex(item, this.selectedItems, this.itemIdPath); | ||
if (index !== -1) { | ||
update.splice(index, 1); | ||
} else { | ||
update.push(item); | ||
} | ||
/** @private */ | ||
_getDisplayValue(selectedItems, itemLabelPath, valueSeparator) { | ||
return selectedItems.map((item) => this._getItemLabel(item, itemLabelPath)).join(valueSeparator); | ||
} | ||
if (!selectedItem) { | ||
this.$.comboBox.value = null; | ||
/** @private */ | ||
_findIndex(item, selectedItems, itemIdPath) { | ||
if (itemIdPath && item) { | ||
for (let index = 0; index < selectedItems.length; index++) { | ||
if (selectedItems[index] && selectedItems[index][itemIdPath] === item[itemIdPath]) { | ||
return index; | ||
} | ||
} | ||
this.selectedItems = update; | ||
if (this.validate()) { | ||
this._dispatchChangeEvent(); | ||
} | ||
// reset the focus index, so a value-change event | ||
// is not fired when the overlay is closed | ||
this.$.comboBox._focusedIndex = -1; | ||
return -1; | ||
} | ||
_handleCustomValueSet(event) { | ||
event.preventDefault(); | ||
if (event.detail) { | ||
this.$.input.value = null; // clear input | ||
const customValuesSetEvent = new CustomEvent('custom-values-set', { | ||
detail: event.detail, | ||
composed: true, | ||
cancelable: true, | ||
bubbles: true | ||
}); | ||
this.dispatchEvent(customValuesSetEvent); | ||
} | ||
} | ||
return selectedItems.indexOf(item); | ||
} | ||
_customIsSelected(item, selectedItem, itemIdPath) { | ||
if (item instanceof ComboBoxPlaceholder) { | ||
return false; | ||
} | ||
return this._isSelected(item, this.selectedItems, itemIdPath); | ||
} | ||
/** @private */ | ||
__clearFilter() { | ||
this.$.comboBox.clear(); | ||
} | ||
_isSelected(item, selectedItems, itemIdPath) { | ||
return this._findIndex(item, selectedItems, itemIdPath) !== -1; | ||
} | ||
/** @private */ | ||
__removeItem(item) { | ||
const itemsCopy = [...this.selectedItems]; | ||
itemsCopy.splice(itemsCopy.indexOf(item), 1); | ||
this.__updateSelection(itemsCopy); | ||
} | ||
_findIndex(item, selectedItems, itemIdPath) { | ||
if (itemIdPath && item) { | ||
for (let index = 0; index < selectedItems.length; index++) { | ||
if (selectedItems[index] && selectedItems[index][itemIdPath] === item[itemIdPath]) { | ||
return index; | ||
} | ||
} | ||
return -1; | ||
} else { | ||
return selectedItems.indexOf(item); | ||
} | ||
} | ||
/** @private */ | ||
__selectItem(item) { | ||
const itemsCopy = [...this.selectedItems]; | ||
_handleItemRemoved(event) { | ||
const item = event.detail.item; | ||
const update = this.selectedItems.slice(0); | ||
update.splice(update.indexOf(item), 1); | ||
this.selectedItems = update; | ||
if (this.validate()) { | ||
this._dispatchChangeEvent(); | ||
} | ||
const index = this._findIndex(item, itemsCopy, this.itemIdPath); | ||
if (index !== -1) { | ||
itemsCopy.splice(index, 1); | ||
} else { | ||
itemsCopy.push(item); | ||
} | ||
_handleRemoveAllItems() { | ||
this.set('selectedItems', []); | ||
if (this.validate()) { | ||
this._dispatchChangeEvent(); | ||
} | ||
} | ||
this.__updateSelection(itemsCopy); | ||
_getReadonlyValue(selectedItems, itemLabelPath, compactMode, readonlyValueSeparator) { | ||
return compactMode ? | ||
this._getCompactModeLabel(selectedItems) : | ||
this._getDisplayValue(selectedItems, itemLabelPath, readonlyValueSeparator); | ||
} | ||
// Reset the overlay focused index. | ||
this.$.comboBox._focusedIndex = -1; | ||
_getDisplayValue(selectedItems, itemLabelPath, valueSeparator) { | ||
return selectedItems.map(item => this._getItemLabel(item, itemLabelPath)).join(valueSeparator); | ||
} | ||
// Suppress `value-changed` event. | ||
this.__clearFilter(); | ||
} | ||
get inputElement() { | ||
return this.$.input; | ||
} | ||
/** @private */ | ||
_sortSelectedItems(selectedItems) { | ||
this.selectedItems = selectedItems.sort((item1, item2) => { | ||
const item1Str = String(this._getItemLabel(item1, this.itemLabelPath)); | ||
const item2Str = String(this._getItemLabel(item2, this.itemLabelPath)); | ||
return item1Str.localeCompare(item2Str); | ||
}); | ||
/** | ||
* Focusable element used by vaadin-control-state-mixin | ||
*/ | ||
get focusElement() { | ||
return this.inputElement; | ||
} | ||
this.__updateChips(); | ||
} | ||
_labelChanged(label) { | ||
this.set('hasLabel', label !== '' && label != null); | ||
} | ||
/** @private */ | ||
__updateSelection(selectedItems) { | ||
this.selectedItems = selectedItems; | ||
_sortSelectedItems(selectedItems) { | ||
selectedItems.sort((item1, item2) => { | ||
const item1Str = String(this._getItemLabel(item1, this.itemLabelPath)); | ||
const item2Str = String(this._getItemLabel(item2, this.itemLabelPath)); | ||
return item1Str.localeCompare(item2Str); | ||
}); | ||
} | ||
this.validate(); | ||
_pageSizeObserver(pageSize, oldPageSize) { | ||
if (Math.floor(pageSize) !== pageSize || pageSize <= 0) { | ||
this.pageSize = oldPageSize; | ||
throw new Error('`pageSize` value must be an integer > 0'); | ||
} | ||
this.$.comboBox.pageSize = pageSize; | ||
} | ||
this.dispatchEvent(new CustomEvent('change', { bubbles: true })); | ||
} | ||
_customOverlaySelectedItemChanged(event) { | ||
event.stopPropagation(); | ||
/** @private */ | ||
__updateChips() { | ||
this.$.repeat.render(); | ||
} | ||
if (event.detail.item instanceof ComboBoxPlaceholder) { | ||
return; | ||
} | ||
/** | ||
* Override method inherited from `ClearButtonMixin` and clear items. | ||
* @protected | ||
* @override | ||
*/ | ||
_onClearButtonClick(event) { | ||
event.stopPropagation(); | ||
if (this.$.comboBox.opened) { | ||
this._comboBoxValueChanged(event, event.detail.item); | ||
this.__updateSelection([]); | ||
} | ||
// When custom values are allowed, we need to clear the input, | ||
// so we don't fire a custom values event | ||
if (this.allowCustomValues) { | ||
this.$.input.value = null; | ||
} | ||
} | ||
/** | ||
* Override an event listener from `KeyboardMixin`. | ||
* @param {KeyboardEvent} event | ||
* @protected | ||
* @override | ||
*/ | ||
_onKeyDown(event) { | ||
const items = this.items || []; | ||
if (!this.compactMode && event.key === 'Backspace' && items.length && this.inputElement.value === '') { | ||
this.__removeItem(items[items.length - 1]); | ||
} | ||
} | ||
_setTemplateFromNodes(nodes) { | ||
this._itemTemplate = nodes.filter(node => node.localName && node.localName === 'template')[0] || this._itemTemplate; | ||
/** @private */ | ||
_onComboBoxChange() { | ||
const item = this.$.comboBox.selectedItem; | ||
if (item) { | ||
this.__selectItem(item); | ||
} | ||
} | ||
_notifyReady() { | ||
this.$server && this.$server.notifyReady(); | ||
} | ||
/** @private */ | ||
_onComboBoxItemSelected(event) { | ||
this.__selectItem(event.detail.item); | ||
} | ||
/** @private */ | ||
_onCustomValueSet(event) { | ||
// Do not set combo-box value | ||
event.preventDefault(); | ||
_notifyResizeIfNeeded() { | ||
if (this.__previousHeight !== undefined && this.__previousHeight !== this.offsetHeight) { | ||
this.notifyResize(); // allows the items drop-down to reposition itself if needed | ||
this.dispatchEvent(new CustomEvent('iron-resize', {bubbles: true})); // allows i.e. vaadin-grid to resize itself | ||
} | ||
this.__previousHeight = this.offsetHeight; | ||
} | ||
this.__clearFilter(); | ||
this.dispatchEvent( | ||
new CustomEvent('custom-values-set', { | ||
detail: event.detail, | ||
composed: true, | ||
bubbles: true | ||
}) | ||
); | ||
} | ||
customElements.define(MultiselectComboBox.is, MultiselectComboBox); | ||
/** @private */ | ||
_onItemRemoved(event) { | ||
this.__removeItem(event.detail.item); | ||
} | ||
/** @private */ | ||
_preventBlur(event) { | ||
// Prevent mousedown event to keep the input focused | ||
// and keep the overlay opened when clicking a chip. | ||
event.preventDefault(); | ||
} | ||
} | ||
customElements.define(MultiselectComboBox.is, MultiselectComboBox); | ||
export { MultiselectComboBox }; |
@@ -0,127 +1,40 @@ | ||
import '@vaadin/vaadin-lumo-styles/color.js'; | ||
import '@vaadin/vaadin-lumo-styles/font-icons.js'; | ||
import '@vaadin/vaadin-lumo-styles/mixins/required-field.js'; | ||
import {html} from '@polymer/polymer/lib/utils/html-tag.js'; | ||
import '@vaadin/vaadin-lumo-styles/style.js'; | ||
import '@vaadin/vaadin-lumo-styles/typography.js'; | ||
import { inputFieldShared } from '@vaadin/vaadin-lumo-styles/mixins/input-field-shared.js'; | ||
import { css, registerStyles } from '@vaadin/vaadin-themable-mixin/vaadin-themable-mixin.js'; | ||
const $_documentContainer = // eslint-disable-line camelcase | ||
html` | ||
<dom-module id="lumo-multiselect-combo-box" theme-for="multiselect-combo-box"> | ||
<template> | ||
<style include="lumo-required-field"> | ||
const multiselectComboBox = css` | ||
:host([has-value]:not([compact-mode]):not([readonly])) { | ||
padding-inline-start: 0; | ||
} | ||
:host { | ||
outline: none; | ||
--lumo-text-field-size: var(--lumo-size-m); | ||
color: var(--lumo-body-text-color); | ||
font-size: var(--lumo-font-size-m); | ||
font-family: var(--lumo-font-family); | ||
-webkit-font-smoothing: antialiased; | ||
-moz-osx-font-smoothing: grayscale; | ||
-webkit-tap-highlight-color: transparent; | ||
padding: var(--lumo-space-xs) 0; | ||
} | ||
[part='chip']:not(:last-of-type) { | ||
margin-inline-end: var(--lumo-space-xs); | ||
} | ||
:host::before { | ||
content: "\\2003"; | ||
width: 0; | ||
box-sizing: border-box; | ||
display: inline-flex; | ||
align-items: center; | ||
} | ||
[part='compact-mode-prefix'] { | ||
box-sizing: border-box; | ||
min-width: 70px; | ||
padding: 0 0.25em; | ||
color: var(--lumo-body-text-color); | ||
font-family: var(--lumo-font-family); | ||
font-weight: 500; | ||
cursor: var(--lumo-clickable-cursor); | ||
} | ||
:host([focused]:not([readonly]):not([disabled])) [part="label"] { | ||
color: var(--lumo-primary-text-color); | ||
} | ||
:host([disabled]) [part='compact-mode-prefix'] { | ||
color: var(--lumo-disabled-text-color); | ||
-webkit-text-fill-color: var(--lumo-disabled-text-color); | ||
pointer-events: none; | ||
} | ||
:host(:hover:not([readonly]):not([focused]):not([disabled])) [part="label"] { | ||
color: var(--lumo-body-text-color); | ||
} | ||
:host([disabled]) [part="label"] { | ||
color: var(--lumo-disabled-text-color); | ||
-webkit-text-fill-color: var(--lumo-disabled-text-color); | ||
} | ||
[part="input-field"] { | ||
background-color: var(--lumo-contrast-10pct); | ||
} | ||
:host(:hover:not([readonly]):not([focused]):not([disabled])) [part="input-field"]::after { | ||
opacity: 0.1; | ||
} | ||
[part="input-field"]::after { | ||
content: ""; | ||
position: absolute; | ||
top: 0; | ||
right: 0; | ||
bottom: 0; | ||
left: 0; | ||
border-radius: inherit; | ||
pointer-events: none; | ||
background-color: var(--lumo-contrast-50pct); | ||
opacity: 0; | ||
transition: transform 0.15s, opacity 0.2s; | ||
transform-origin: 100% 0; | ||
} | ||
:host([invalid]) [part="input-field"] { | ||
background-color: var(--lumo-error-color-10pct); | ||
} | ||
/* Trigger when not focusing using the keyboard */ | ||
:host([focused]:not([focus-ring]):not([readonly])) [part="input-field"]::after { | ||
transform: scaleX(0); | ||
transition-duration: 0.2s, 1s; | ||
} | ||
[part="readonly-container"]:not([hidden]) { | ||
display: inline-flex; | ||
align-items: center; | ||
color: var(--lumo-secondary-text-color); | ||
border: 1px dashed var(--lumo-contrast-30pct); | ||
border-radius: var(--lumo-border-radius); | ||
padding: 0 calc(0.375em + var(--lumo-border-radius) / 4 - 1px); | ||
font-weight: 500; | ||
min-height: var(--lumo-text-field-size); | ||
cursor: default; | ||
} | ||
</style> | ||
</template> | ||
</dom-module> | ||
<dom-module id="lumo-combo-box-item-theme" theme-for="vaadin-combo-box-item"> | ||
<template> | ||
<style> | ||
[part="content"] { | ||
font-size: var(--lumo-font-size-s); | ||
} | ||
</style> | ||
</template> | ||
<dom-module> | ||
<dom-module id="lumo-input-field-theme" theme-for="vaadin-text-field"> | ||
<template> | ||
<style> | ||
:host(.multiselect) [part="input-field"], | ||
:host(.multiselect) [part="input-field"]::after { | ||
background-color: transparent !important; | ||
box-shadow: none; | ||
} | ||
:host(.multiselect[compact-mode]) [part="input-field"] { | ||
cursor: default; | ||
} | ||
:host(.multiselect[compact-mode]) [part="input-field"]::after { | ||
border: none; | ||
} | ||
:host(.multiselect[compact-mode]) [part="input-field"] [part="value"] { | ||
visibility: hidden; | ||
} | ||
</style> | ||
</template> | ||
</dom-module> | ||
[part='toggle-button']::before { | ||
content: var(--lumo-icons-dropdown); | ||
} | ||
`; | ||
document.head.appendChild($_documentContainer.content); | ||
registerStyles('multiselect-combo-box', [inputFieldShared, multiselectComboBox], { | ||
moduleId: 'lumo-multiselect-combo-box' | ||
}); |
@@ -1,5 +0,6 @@ | ||
import '@vaadin/vaadin-text-field/theme/lumo/vaadin-text-field.js'; | ||
import '@vaadin/vaadin-combo-box/theme/lumo/vaadin-combo-box-light.js'; | ||
import '@vaadin/combo-box/theme/lumo/vaadin-combo-box-item-styles.js'; | ||
import '@vaadin/combo-box/theme/lumo/vaadin-combo-box-dropdown-styles.js'; | ||
import '@vaadin/input-container/theme/lumo/vaadin-input-container.js'; | ||
import './multiselect-combo-box-chip-styles.js'; | ||
import './multiselect-combo-box-styles.js'; | ||
import './multiselect-combo-box-input.js'; | ||
import '../../src/multiselect-combo-box.js'; |
@@ -0,158 +1,41 @@ | ||
import '@vaadin/vaadin-material-styles/color.js'; | ||
import '@vaadin/vaadin-material-styles/font-icons.js'; | ||
import '@vaadin/vaadin-material-styles/mixins/required-field.js'; | ||
import '@vaadin/vaadin-material-styles/color.js'; | ||
import {html} from '@polymer/polymer/lib/utils/html-tag.js'; | ||
import '@vaadin/vaadin-material-styles/typography.js'; | ||
import { inputFieldShared } from '@vaadin/vaadin-material-styles/mixins/input-field-shared.js'; | ||
import { css, registerStyles } from '@vaadin/vaadin-themable-mixin/vaadin-themable-mixin.js'; | ||
const $_documentContainer = // eslint-disable-line camelcase | ||
html` | ||
<dom-module id="material-multiselect-combo-box" theme-for="multiselect-combo-box"> | ||
<template> | ||
<style include="material-required-field"> | ||
:host { | ||
outline: none; | ||
position: relative; | ||
} | ||
const multiSelectComboBox = css` | ||
[part='input-field'] { | ||
height: auto; | ||
min-height: 32px; | ||
} | ||
:host([has-label]) { | ||
padding-top: 24px; | ||
} | ||
[part='input-field'] ::slotted(input) { | ||
padding: 6px 0; | ||
} | ||
:host([focused]:not([readonly]):not([invalid]):not([disabled])) [part="label"] { | ||
color: var(--material-primary-text-color); | ||
} | ||
[part='compact-mode-prefix'] { | ||
color: var(--material-body-text-color); | ||
font-family: var(--material-font-family); | ||
font-size: var(--material-body-font-size); | ||
cursor: default; | ||
} | ||
:host(:hover:not([readonly]):not([focused]):not([invalid]):not([disabled])) [part="input-field"]::after { | ||
opacity: 0.1; | ||
} | ||
:host([disabled]) [part='compact-mode-prefix'] { | ||
color: var(--material-disabled-text-color); | ||
-webkit-text-fill-color: var(--material-disabled-text-color); | ||
pointer-events: none; | ||
} | ||
:host(:hover:not([readonly]):not([invalid]):not([disabled])) [part="input-field"]::before { | ||
opacity: var(--_material-text-field-input-line-hover-opacity, 0.87); | ||
} | ||
[part='toggle-button']::before { | ||
content: var(--material-icons-dropdown); | ||
} | ||
:host([focused]) [part="input-field"]::after, | ||
:host([invalid]) [part="input-field"]::after { | ||
opacity: 1; | ||
transform: none; | ||
transition: transform 0.175s, opacity 0.175s; | ||
} | ||
:host([invalid]) [part="input-field"]::after { | ||
background-color: var(--material-error-color); | ||
} | ||
:host([has-label]:not([has-value]):not([focused]):not([invalid]):not([theme="always-float-label"]):not([compact-mode]):not([disabled])) [part="label"] { | ||
transform: scale(1) translateY(24px); | ||
transition-timing-function: ease, ease, step-start; | ||
pointer-events: none; | ||
left: auto; | ||
transition-delay: 0.1s; | ||
} | ||
[part="label"] { | ||
transition: transform 0.175s, color 0.175s, width 0.175s; | ||
transition-timing-function: ease, ease, step-end; | ||
} | ||
[part="label"]:empty::before { | ||
content: " "; | ||
position: absolute; | ||
} | ||
[part="readonly-container"]:not([hidden]) { | ||
display: inline-flex; | ||
align-items: center; | ||
color: var(--material-body-text-color); | ||
border: 1px dashed var(--material-text-field-input-line-background-color, #000); | ||
padding: 0 var(--material-space-s); | ||
min-height: 32px; | ||
cursor: default; | ||
} | ||
</style> | ||
</template> | ||
</dom-module> | ||
<dom-module id="material-combo-box-item-theme" theme-for="vaadin-combo-box-item"> | ||
<template> | ||
<style> | ||
[part="content"] { | ||
font-size: var(--material-small-font-size); | ||
} | ||
</style> | ||
</template> | ||
<dom-module> | ||
<dom-module id="material-input-field-theme" theme-for="vaadin-text-field"> | ||
<template> | ||
<style> | ||
:host(.multiselect) [part="input-field"], | ||
:host(.multiselect) [part="input-field"]::after { | ||
background-color: transparent; | ||
font-size: var(--material-small-font-size); | ||
} | ||
:host(.multiselect) [part="input-field"]::before { | ||
display: none; | ||
} | ||
:host(.multiselect[compact-mode]) [part="input-field"] { | ||
cursor: default; | ||
} | ||
:host(.multiselect[compact-mode]) [part="input-field"] [part="value"] { | ||
visibility: hidden; | ||
} | ||
/* placeholder styles */ | ||
:host(.multiselect) [part="input-field"] [part="value"]::-webkit-input-placeholder { | ||
color: var(--material-disabled-text-color); | ||
transition: opacity 0.175s 0.05s; | ||
opacity: 1; | ||
} | ||
:host(.multiselect) [part="input-field"] [part="value"]::-moz-placeholder { | ||
color: var(--material-disabled-text-color); | ||
transition: opacity 0.175s 0.05s; | ||
opacity: 1; | ||
} | ||
:host(.multiselect) [part="input-field"] [part="value"]:-ms-input-placeholder { | ||
color: var(--material-disabled-text-color); | ||
transition: opacity 0.175s 0.05s; | ||
opacity: 1; | ||
} | ||
:host(.multiselect) [part="input-field"] [part="value"]::placeholder { | ||
color: var(--material-disabled-text-color); | ||
transition: opacity 0.175s 0.05s; | ||
opacity: 1; | ||
} | ||
:host(.multiselect[multiselect-has-label]:not([focused]):not([invalid]):not([theme="always-float-label"])) [part="input-field"] [part="value"]::-webkit-input-placeholder, | ||
:host(.multiselect[multiselect-has-value]) [part="input-field"] [part="value"]::-webkit-input-placeholder { | ||
opacity: 0; | ||
transition-delay: 0; | ||
} | ||
:host(.multiselect[multiselect-has-label]:not([focused]):not([invalid]):not([theme="always-float-label"])) [part="input-field"] [part="value"]::-moz-placeholder, | ||
:host(.multiselect[multiselect-has-value]) [part="input-field"] [part="value"]::-moz-placeholder { | ||
opacity: 0; | ||
transition-delay: 0; | ||
} | ||
:host(.multiselect[multiselect-has-label]:not([focused]):not([invalid]):not([theme="always-float-label"])) [part="input-field"] [part="value"]:-ms-input-placeholder, | ||
:host(.multiselect[multiselect-has-value]) [part="input-field"] [part="value"]:-ms-input-placeholder { | ||
opacity: 0; | ||
transition-delay: 0; | ||
} | ||
:host(.multiselect[multiselect-has-label]:not([focused]):not([invalid]):not([theme="always-float-label"])) [part="input-field"] [part="value"]::placeholder, | ||
:host(.multiselect[multiselect-has-value]) [part="input-field"] [part="value"]::placeholder { | ||
opacity: 0; | ||
transition-delay: 0; | ||
} | ||
</style> | ||
</template> | ||
</dom-module> | ||
:host([opened]) [part='toggle-button'] { | ||
transform: rotate(180deg); | ||
} | ||
`; | ||
document.head.appendChild($_documentContainer.content); | ||
registerStyles('multiselect-combo-box', [inputFieldShared, multiSelectComboBox], { | ||
moduleId: 'multiselect-combo-box' | ||
}); |
@@ -1,5 +0,6 @@ | ||
import '@vaadin/vaadin-text-field/theme/material/vaadin-text-field.js'; | ||
import '@vaadin/vaadin-combo-box/theme/material/vaadin-combo-box-light.js'; | ||
import '@vaadin/combo-box/theme/material/vaadin-combo-box-item-styles.js'; | ||
import '@vaadin/combo-box/theme/material/vaadin-combo-box-dropdown-styles.js'; | ||
import '@vaadin/input-container/theme/material/vaadin-input-container.js'; | ||
import './multiselect-combo-box-chip-styles.js'; | ||
import './multiselect-combo-box-styles.js'; | ||
import './multiselect-combo-box-input.js'; | ||
import '../../src/multiselect-combo-box.js'; |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
63251
22
1348
10
32
55
1
+ Added@open-wc/dedupe-mixin@^1.3.0
+ Added@vaadin/combo-box@^22.0.0
+ Added@vaadin/field-base@^22.0.0
+ Added@lit-labs/ssr-dom-shim@1.2.1(transitive)
+ Added@lit/reactive-element@1.6.3(transitive)
+ Added@open-wc/dedupe-mixin@1.4.0(transitive)
+ Added@types/trusted-types@2.0.7(transitive)
+ Added@vaadin/combo-box@22.1.0(transitive)
+ Added@vaadin/component-base@22.1.0(transitive)
+ Added@vaadin/field-base@22.1.0(transitive)
+ Added@vaadin/icon@22.1.0(transitive)
+ Added@vaadin/input-container@22.1.0(transitive)
+ Added@vaadin/item@22.1.0(transitive)
+ Added@vaadin/polymer-legacy-adapter@22.1.0(transitive)
+ Added@vaadin/vaadin-lumo-styles@22.1.0(transitive)
+ Added@vaadin/vaadin-material-styles@22.1.0(transitive)
+ Added@vaadin/vaadin-overlay@22.1.0(transitive)
+ Added@vaadin/vaadin-themable-mixin@22.1.0(transitive)
+ Addedlit@2.8.0(transitive)
+ Addedlit-element@3.3.3(transitive)
+ Addedlit-html@2.8.0(transitive)
- Removed@vaadin/vaadin-combo-box@^5.0.9
- Removed@vaadin/vaadin-text-field@^2.4.8
- Removed@polymer/iron-a11y-announcer@3.2.0(transitive)
- Removed@polymer/iron-a11y-keys-behavior@3.0.1(transitive)
- Removed@polymer/iron-list@3.1.0(transitive)
- Removed@polymer/iron-resizable-behavior@3.0.1(transitive)
- Removed@polymer/iron-scroll-target-behavior@3.0.1(transitive)
- Removed@vaadin/vaadin-combo-box@5.5.3(transitive)
- Removed@vaadin/vaadin-control-state-mixin@2.2.6(transitive)
- Removed@vaadin/vaadin-element-mixin@2.4.2(transitive)
- Removed@vaadin/vaadin-item@2.3.0(transitive)
- Removed@vaadin/vaadin-lumo-styles@1.6.1(transitive)
- Removed@vaadin/vaadin-material-styles@1.3.2(transitive)
- Removed@vaadin/vaadin-overlay@3.5.1(transitive)
- Removed@vaadin/vaadin-text-field@2.10.0(transitive)
- Removed@vaadin/vaadin-themable-mixin@1.6.2(transitive)
- Removedlit-element@2.5.1(transitive)
- Removedlit-html@1.4.1(transitive)