Styles imported. Components added

pull/3420/head
Bahadir Sofuoglu 2022-09-06 15:03:37 +03:00
parent 3cb3910561
commit 8264983db4
452 changed files with 55902 additions and 0 deletions

90
components/accordion/Accordion.d.ts vendored Executable file
View File

@ -0,0 +1,90 @@
import { VNode } from 'vue';
import { ClassComponent, GlobalComponentConstructor } from '../ts-helpers';
export interface AccordionTabOpenEvent {
/**
* Browser mouse event.
* @type {MouseEvent}
*/
originalEvent: MouseEvent;
/**
* Opened tab index.
*/
index: number;
}
/**
* @extends {AccordionTabOpenEvent}
*/
export interface AccordionTabCloseEvent extends AccordionTabOpenEvent { }
export interface AccordionProps {
/**
* When enabled, multiple tabs can be activated at the same time.
*/
multiple?: boolean | undefined;
/**
* Index of the active tab or an array of indexes in multiple mode.
*/
activeIndex?: number | number[] | undefined;
/**
* When enabled, hidden tabs are not rendered at all. Defaults to false that hides tabs with css.
*/
lazy?: boolean | undefined;
/**
* Icon of a collapsed tab.
*/
expandIcon?: string | undefined;
/**
* Icon of an expanded tab.
*/
collapseIcon?: string | undefined;
}
export interface AccordionSlots {
/**
* Default slot to detect AccordionTab components.
*/
default: () => VNode[];
}
export declare type AccordionEmits = {
/**
* Emitted when the active tab changes.
* @param {number | undefined} value - Index of new active tab.
*/
'update:activeIndex': (value: number | undefined) => void;
/**
* Callback to invoke when a tab gets expanded.
* @param {AccordionTabOpenEvent} event - Custom tab open event.
*/
'tab-open': (event: AccordionTabOpenEvent) => void;
/**
* Callback to invoke when an active tab is collapsed by clicking on the header.
* @param {AccordionTabCloseEvent} event - Custom tab close event.
*/
'tab-close': (event: AccordionTabCloseEvent) => void;
}
declare class Accordion extends ClassComponent<AccordionProps, AccordionSlots, AccordionEmits> { }
declare module '@vue/runtime-core' {
interface GlobalComponents {
Accordion: GlobalComponentConstructor<Accordion>
}
}
/**
*
* Accordion groups a collection of contents in tabs.
*
* Helper Components:
*
* - AccordionTab
*
* Demos:
*
* - [Accordion](https://www.primefaces.org/primevue/showcase/#/accordion)
*
*/
export default Accordion;

View File

@ -0,0 +1,50 @@
import { mount } from '@vue/test-utils';
import Accordion from '@/components/accordion/Accordion.vue';
import AccordionTab from '@/components/accordiontab/AccordionTab.vue';
describe('Accordion.vue', () => {
let wrapper;
beforeEach(() => {
wrapper = mount(Accordion, {
global: {
components: {
AccordionTab
}
},
slots: {
default: `
<AccordionTab header="Header I">
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation
ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.
Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</p>
</AccordionTab>
<AccordionTab header="Header II">
<p>Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi
architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos qui ratione
voluptatem sequi nesciunt. Consectetur, adipisci velit, sed quia non numquam eius modi.</p>
</AccordionTab>
<AccordionTab header="Header III">
<p>At vero eos et accusamus et iusto odio dignissimos ducimus qui blanditiis praesentium voluptatum deleniti atque corrupti quos dolores et quas molestias excepturi sint occaecati
cupiditate non provident, similique sunt in culpa qui officia deserunt mollitia animi, id est laborum et dolorum fuga. Et harum quidem rerum facilis est et expedita distinctio.
Nam libero tempore, cum soluta nobis est eligendi optio cumque nihil impedit quo minus.</p>
</AccordionTab>`
}
});
});
it('should Accordion and AccordionTab component exist', () => {
expect(wrapper.find('.p-accordion.p-component').exists()).toBe(true);
expect(wrapper.find('.p-accordion-tab').exists()).toBe(true);
expect(wrapper.findAll('.p-accordion-tab').length).toBe(3);
});
it('should activeIndex change', async() => {
await wrapper.setProps({ activeIndex: 1 });
const allTabs = wrapper.findAll('.p-accordion-tab');
expect(allTabs[0].classes()).not.toContain('p-accordion-tab-active');
expect(allTabs[1].classes()).toContain('p-accordion-tab-active');
});
});

View File

@ -0,0 +1,169 @@
<template>
<div class="p-accordion p-component">
<div v-for="(tab,i) of tabs" :key="getKey(tab,i)" :class="getTabClass(i)">
<div :class="getTabHeaderClass(tab, i)">
<a role="tab" class="p-accordion-header-link" @click="onTabClick($event, tab, i)" @keydown="onTabKeydown($event, tab, i)" :tabindex="isTabDisabled(tab) ? null : '0'"
:aria-expanded="isTabActive(i)" :id="getTabAriaId(i) + '_header'" :aria-controls="getTabAriaId(i) + '_content'">
<span :class="isTabActive(i) ? getHeaderCollapseIcon() : getHeaderExpandIcon()"></span>
<span class="p-accordion-header-text" v-if="tab.props && tab.props.header">{{tab.props.header}}</span>
<component :is="tab.children.header" v-if="tab.children && tab.children.header"></component>
</a>
</div>
<transition name="p-toggleable-content">
<div class="p-toggleable-content" v-if="lazy ? isTabActive(i) : true" v-show="lazy ? true: isTabActive(i)"
role="region" :id="getTabAriaId(i) + '_content'" :aria-labelledby="getTabAriaId(i) + '_header'">
<div class="p-accordion-content">
<component :is="tab"></component>
</div>
</div>
</transition>
</div>
</div>
</template>
<script>
import {UniqueComponentId} from 'primevue/utils';
export default {
name: 'Accordion',
emits: ['tab-close', 'tab-open', 'update:activeIndex'],
props: {
multiple: {
type: Boolean,
default: false
},
activeIndex: {
type: [Number,Array],
default: null
},
lazy: {
type: Boolean,
default: false
},
expandIcon: {
type: String,
default: 'pi-chevron-right'
},
collapseIcon: {
type: String,
default: 'pi-chevron-down'
}
},
data() {
return {
d_activeIndex: this.activeIndex
}
},
watch: {
activeIndex(newValue) {
this.d_activeIndex = newValue;
}
},
methods: {
onTabClick(event, tab, i) {
if (!this.isTabDisabled(tab)) {
const active = this.isTabActive(i);
const eventName = active ? 'tab-close' : 'tab-open';
if (this.multiple) {
if (active) {
this.d_activeIndex = this.d_activeIndex.filter(index => index !== i);
}
else {
if (this.d_activeIndex)
this.d_activeIndex.push(i);
else
this.d_activeIndex = [i];
}
}
else {
this.d_activeIndex = this.d_activeIndex === i ? null : i;
}
this.$emit('update:activeIndex', this.d_activeIndex);
this.$emit(eventName, {
originalEvent: event,
index: i
});
}
},
onTabKeydown(event, tab, i) {
if (event.which === 13) {
this.onTabClick(event, tab, i);
}
},
isTabActive(index) {
if (this.multiple)
return this.d_activeIndex && this.d_activeIndex.includes(index);
else
return index === this.d_activeIndex;
},
getKey(tab, i) {
return (tab.props && tab.props.header) ? tab.props.header : i;
},
isTabDisabled(tab) {
return tab.props && tab.props.disabled;
},
getTabClass(i) {
return ['p-accordion-tab', {'p-accordion-tab-active': this.isTabActive(i)}];
},
getTabHeaderClass(tab, i) {
return ['p-accordion-header', {'p-highlight': this.isTabActive(i), 'p-disabled': this.isTabDisabled(tab)}];
},
getTabAriaId(i) {
return this.ariaId + '_' + i;
},
getHeaderCollapseIcon() {
return ['p-accordion-toggle-icon pi', this.collapseIcon];
},
getHeaderExpandIcon() {
return ['p-accordion-toggle-icon pi', this.expandIcon];
},
isAccordionTab(child) {
return child.type.name === 'AccordionTab';
}
},
computed: {
tabs() {
const tabs = []
this.$slots.default().forEach(child => {
if (this.isAccordionTab(child)) {
tabs.push(child);
}
else if (child.children && child.children instanceof Array) {
child.children.forEach(nestedChild => {
if (this.isAccordionTab(nestedChild)) {
tabs.push(nestedChild)
}
});
}
}
)
return tabs;
},
ariaId() {
return UniqueComponentId();
}
}
}
</script>
<style>
.p-accordion-header-link {
cursor: pointer;
display: flex;
align-items: center;
user-select: none;
position: relative;
text-decoration: none;
}
.p-accordion-header-link:focus {
z-index: 1;
}
.p-accordion-header-text {
line-height: 1;
}
</style>

View File

@ -0,0 +1,9 @@
{
"main": "./accordion.cjs.js",
"module": "./accordion.esm.js",
"unpkg": "./accordion.min.js",
"types": "./Accordion.d.ts",
"browser": {
"./sfc": "./Accordion.vue"
}
}

45
components/accordiontab/AccordionTab.d.ts vendored Executable file
View File

@ -0,0 +1,45 @@
import { VNode } from 'vue';
import { ClassComponent, GlobalComponentConstructor } from '../ts-helpers';
export interface AccordionTabProps {
/**
* Orientation of tab headers.
*/
header?: string | undefined;
/**
* Whether the tab is disabled.
*/
disabled?: boolean | undefined;
}
export interface AccordionTabSlots {
/**
* Default slot for content.
*/
default: () => VNode[];
/**
* Custom content for the title section of a panel is defined using the header template.
*/
header: () => VNode[];
}
export declare type AccordionTabEmits = { }
declare class AccordionTab extends ClassComponent<AccordionTabProps, AccordionTabSlots, AccordionTabEmits> { }
declare module '@vue/runtime-core' {
interface GlobalComponents {
AccordionTab: GlobalComponentConstructor<AccordionTab>
}
}
/**
*
* AccordionTab is a helper component for Accordion.
*
* Demos:
*
* - [Accordion](https://www.primefaces.org/primevue/showcase/#/accordion)
*
*/
export default AccordionTab;

View File

@ -0,0 +1,14 @@
import { mount } from '@vue/test-utils';
import AccordionTab from '@/components/accordiontab/AccordionTab.vue';
describe('AccordionTab.vue', () => {
it('should exists', () => {
const wrapper = mount(AccordionTab, {
slots: {
default: '<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do</p>'
}
});
expect(wrapper.text()).toBe('Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do');
});
});

View File

@ -0,0 +1,13 @@
<template>
<slot></slot>
</template>
<script>
export default {
name: 'AccordionTab',
props: {
header: null,
disabled: Boolean
}
}
</script>

View File

@ -0,0 +1,9 @@
{
"main": "./accordiontab.cjs.js",
"module": "./accordiontab.esm.js",
"unpkg": "./accordiontab.min.js",
"types": "./AccordionTab.d.ts",
"browser": {
"./sfc": "./AccordionTab.vue"
}
}

447
components/autocomplete/AutoComplete.d.ts vendored Executable file
View File

@ -0,0 +1,447 @@
import { HTMLAttributes, InputHTMLAttributes, VNode } from 'vue';
import { ClassComponent, GlobalComponentConstructor } from '../ts-helpers';
import { VirtualScrollerProps, VirtualScrollerItemOptions } from '../virtualscroller';
type AutoCompleteFieldType = string | ((data: any) => string) | undefined;
type AutoCompleteOptionLabelType = string | ((data: any) => string) | undefined;
type AutoCompleteOptionDisabledType = string | ((data: any) => boolean) | undefined;
type AutoCompleteOptionChildrenType = string | ((data: any) => any[]) | undefined;
type AutoCompleteDropdownMode = 'blank' | 'current' | undefined;
type AutoCompleteAppendTo = 'body' | 'self' | string | undefined | HTMLElement;
export interface AutoCompleteChangeEvent {
/**
* Browser event.
*/
originalEvent: Event;
/**
* Selected option value
*/
value: any;
}
export interface AutoCompleteItemSelectEvent {
/**
* Browser event
*/
originalEvent: Event;
/**
* Selected item
*/
value: any;
}
/**
* @extends AutoCompleteItemSelectEvent
*/
export interface AutoCompleteItemUnselectEvent extends AutoCompleteItemSelectEvent { }
export interface AutoCompleteDropdownClickEvent {
/**
* Browser mouse event
*/
originalEvent: MouseEvent;
/**
* Current value of the input field
*/
query: string;
}
export interface AutoCompleteCompleteEvent {
/**
* Browser event
*/
originalEvent: Event;
/**
* Value to search with
*/
query: string;
}
export interface AutoCompleteProps {
/**
* Value of the component.
*/
modelValue?: any;
/**
* An array of suggestions to display.
*/
suggestions?: any[];
/**
* @deprecated since v3.16.0. Use 'optionLabel' property instead.
* Property name or getter function of a suggested object to resolve and display.
* @type {AutoCompleteFieldType}
*/
field?: AutoCompleteFieldType;
/**
* Property name or getter function to use as the label of an option.
* @see AutoCompleteOptionLabelType
*/
optionLabel?: AutoCompleteOptionLabelType;
/**
* Property name or getter function to use as the disabled flag of an option, defaults to false when not defined.
* @see AutoCompleteOptionDisabledType
*/
optionDisabled?: AutoCompleteOptionDisabledType;
/**
* Property name or getter function to use as the label of an option group.
* @see AutoCompleteOptionLabelType
*/
optionGroupLabel?: AutoCompleteOptionLabelType;
/**
* Property name or getter function that refers to the children options of option group.
* @see AutoCompleteOptionChildrenType
*/
optionGroupChildren?: AutoCompleteOptionChildrenType;
/**
* Maximum height of the suggestions panel.
* Default value is '200px'.
*/
scrollHeight?: string | undefined;
/**
* Displays a button next to the input field when enabled.
*/
dropdown?: boolean | undefined;
/**
* Specifies the behavior dropdown button. Default 'blank' mode sends an empty string and 'current' mode sends the input value.
* @see AutoCompleteDropdownMode
* Default value is 'blank'.
*/
dropdownMode?: AutoCompleteDropdownMode;
/**
* @deprecated since v3.16.0
* Highlights automatically the first item of the dropdown to be selected.
*/
autoHighlight?: boolean | undefined;
/**
* Specifies if multiple values can be selected.
*/
multiple?: boolean | undefined;
/**
* Default text to display when no option is selected.
*/
placeholder?: string | undefined;
/**
* When present, it specifies that the component should be disabled.
*/
disabled?: boolean | undefined;
/**
* A property to uniquely identify an option.
*/
dataKey?: string | undefined;
/**
* Minimum number of characters to initiate a search.
* Default value is 1.
*/
minLength?: number | undefined;
/**
* Delay between keystrokes to wait before sending a query.
* Default value is 300.
*/
delay?: number | undefined;
/**
* A valid query selector or an HTMLElement to specify where the overlay gets attached.
* Special keywords are 'body' for document body and 'self' for the element itself.
* @see AutoCompleteAppendTo
* Default value is body.
*/
appendTo?: AutoCompleteAppendTo;
/**
* When present, autocomplete clears the manual input if it does not match of the suggestions to force only accepting values from the suggestions.
*/
forceSelection?: boolean;
/**
* Whether to run a query when input receives focus.
*/
completeOnFocus?: boolean | undefined;
/**
* Identifier of the underlying input element.
*/
inputId?: string | undefined;
/**
* Inline style of the input field.
*/
inputStyle?: any;
/**
* Style class of the input field.
*/
inputClass?: any;
/**
* Uses to pass all properties of the HTMLInputElement to the focusable input element inside the component.
*/
inputProps?: InputHTMLAttributes | undefined;
/**
* Inline style of the overlay panel.
*/
panelStyle?: any;
/**
* Style class of the overlay panel.
*/
panelClass?: any;
/**
* Uses to pass all properties of the HTMLDivElement to the overlay panel inside the component.
*/
panelProps?: HTMLAttributes | undefined;
/**
* Icon to display in loading state.
* Default value is 'pi pi-spinner pi-spin'.
*/
loadingIcon?: string | undefined;
/**
* Whether to use the virtualScroller feature. The properties of VirtualScroller component can be used like an object in it.
* @see VirtualScroller.VirtualScrollerProps
*/
virtualScrollerOptions?: VirtualScrollerProps;
/**
* Whether to focus on the first visible or selected element when the overlay panel is shown.
* Default value is true.
*/
autoOptionFocus?: boolean | undefined;
/**
* Locale to use in searching. The default locale is the host environment's current locale.
*/
searchLocale?: string | undefined;
/**
* Text to be displayed in hidden accessible field when filtering returns any results. Defaults to value from PrimeVue locale configuration.
* Default value is '{0} results are available'.
*/
searchMessage?: string | undefined;
/**
* Text to be displayed in hidden accessible field when options are selected. Defaults to value from PrimeVue locale configuration.
* Default value is '{0} items selected'.
*/
selectionMessage?: string | undefined;
/**
* Text to be displayed in hidden accessible field when any option is not selected. Defaults to value from PrimeVue locale configuration.
* Default value is 'No selected item'.
*/
emptySelectionMessage?: string | undefined;
/**
* Text to display when filtering does not return any results. Defaults to value from PrimeVue locale configuration.
* Default value is 'No results found'.
*/
emptySearchMessage?: string | undefined;
/**
* Index of the element in tabbing order.
*/
tabindex?: number | string | undefined;
/**
* Defines a string value that labels an interactive element.
*/
"aria-label"?: string | undefined;
/**
* Identifier of the underlying input element.
*/
"aria-labelledby"?: string | undefined;
}
export interface AutoCompleteSlots {
/**
* Custom chip template.
* @param {Object} scope - chip slot's params.
*/
chip: (scope: {
/**
* A value in the selection
*/
value: any;
}) => VNode[];
/**
* Custom header template of panel.
* @param {Object} scope - header slot's params.
*/
header: (scope: {
/**
* Value of the component
*/
value: any;
/**
* Displayed options
*/
suggestions: any[];
}) => VNode[];
/**
* Custom footer template of panel.
* @param {Object} scope - footer slot's params.
*/
footer: (scope: {
/**
* Value of the component
*/
value: any;
/**
* Displayed options
*/
suggestions: any[];
}) => VNode[];
/**
* @deprecated since v3.16.0
* Custom content for each item.
* @param {Object} scope - item slot's params.
*/
item: (scope: {
/**
* Option instance
*/
item: any;
/**
* Index of the option
*/
index: number;
}) => VNode[];
/**
* Custom option template.
* @param {Object} scope - option slot's params.
*/
option: (scope: {
/**
* Option instance
*/
option: any;
/**
* Index of the option
*/
index: number;
}) => VNode[];
/**
* Custom option group template.
* @param {Object} scope - option group slot's params.
*/
optiongroup: (scope: {
/**
* @deprecated since v3.16.0. Use the 'option' instead.
* Option instance
*/
item: any;
/**
* Option instance
*/
option: any;
/**
* Index of the option
*/
index: number;
}) => VNode[];
/**
* Custom panel template.
* @param {Object} scope - content slot's params.
*/
content: (scope: {
/**
* An array of objects to display for virtualscroller
*/
items: any;
/**
* Style class of the component
*/
styleClass: string;
/**
* Referance of the content
* @param {HTMLElement} el - Element of 'ref' property
*/
contentRef(el: any): void;
/**
* Options of the items
* @param {number} index - Rendered index
* @return {@link VirtualScroller.VirtualScrollerItemOptions}
*/
getItemOptions(index: number): VirtualScrollerItemOptions;
}) => VNode[];
/**
* Custom loader template.
* @param {Object} scope - loader slot's params.
*/
loader: (scope: {
/**
* Options of the loader items for virtualscroller
*/
options: any[];
}) => VNode[];
}
export declare type AutoCompleteEmits = {
/**
* Emitted when the value changes.
* @param {*} value - New value.
*/
'update:modelValue': (value: any) => void;
/**
* Callback to invoke on value change.
* @param {AutoCompleteChangeEvent} event - Custom change event.
*/
'change': (event: AutoCompleteChangeEvent) => void;
/**
* Callback to invoke when the component receives focus.
* @param {Event} event - Browser event.
*/
'focus': (event: Event) => void;
/**
* Callback to invoke when the component loses focus.
* @param {Event} event - Browser event.
*/
'blur': (event: Event) => void;
/**
* Callback to invoke when a suggestion is selected.
* @param {AutoCompleteItemSelectEvent} event - Custom item select event.
*/
'item-select': (event: AutoCompleteItemSelectEvent) => void;
/**
* Callback to invoke when a selected value is removed.
* @param {AutoCompleteItemUnselectEvent} event - Custom item unselect event.
*/
'item-unselect': (event: AutoCompleteItemUnselectEvent) => void;
/**
* Callback to invoke to when dropdown button is clicked.
* @param {AutoCompleteDropdownClickEvent} event - Custom dropdown click event.
*/
'dropdown-click': (event: AutoCompleteDropdownClickEvent) => void;
/**
* Callback to invoke when input is cleared by the user.
*/
'clear': () => void;
/**
* Callback to invoke to search for suggestions.
* @param {AutoCompleteCompleteEvent} event - Custom complete event.
*/
'complete': (event: AutoCompleteCompleteEvent) => void;
/**
* Callback to invoke before the overlay is shown.
*/
'before-show': () => void;
/**
* Callback to invoke before the overlay is hidden.
*/
'before-hide': () => void;
/**
* Callback to invoke when the overlay is shown.
*/
'show': () => void;
/**
* Callback to invoke when the overlay is hidden.
*/
'hide': () => void;
}
declare class AutoComplete extends ClassComponent<AutoCompleteProps, AutoCompleteSlots, AutoCompleteEmits> { }
declare module '@vue/runtime-core' {
interface GlobalComponents {
AutoComplete: GlobalComponentConstructor<AutoComplete>
}
}
/**
*
* AutoComplete is an input component that provides real-time suggestions when being typed.
*
* Demos:
*
* - [AutoComplete](https://www.primefaces.org/primevue/showcase/#/autocomplete)
*
*/
export default AutoComplete;

View File

@ -0,0 +1,58 @@
import { mount } from '@vue/test-utils';
import PrimeVue from '@/components/config/PrimeVue';
import AutoComplete from './AutoComplete.vue';
describe('AutoComplete.vue', () => {
let wrapper;
beforeEach(async () => {
wrapper = mount(AutoComplete, {
global: {
plugins: [PrimeVue],
stubs: {
teleport: true
}
},
props: {
suggestions: null,
field: 'name'
},
data() {
return {
countries: [
{"name": "Afghanistan", "code": "AF"},
{"name": "Bahrain", "code": "BH"},
{"name": "Chile", "code": "CL"},
{"name": "Denmark", "code": "DK"}
]
}
}
});
await wrapper.trigger('click');
});
it('should exists', () => {
expect(wrapper.find('.p-autocomplete.p-component').exists()).toBe(true);
expect(wrapper.find('.p-autocomplete-input').exists()).toBe(true);
});
it('search copmlete', async () => {
const event = {'target': { 'value': 'b' }};
wrapper.vm.onInput(event);
await wrapper.vm.$nextTick();
wrapper.vm.search(event , event.target.value, 'input');
await wrapper.vm.$nextTick();
await wrapper.setProps({
suggestions: [
{"name": "Bahrain", "code": "BH"}
]
});
expect(wrapper.find('.p-autocomplete-items').exists()).toBe(true);
expect(wrapper.findAll('.p-autocomplete-item').length).toBe(1);
});
});

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,9 @@
{
"main": "./autocomplete.cjs.js",
"module": "./autocomplete.esm.js",
"unpkg": "./autocomplete.min.js",
"types": "./AutoComplete.d.ts",
"browser": {
"./sfc": "./AutoComplete.vue"
}
}

66
components/avatar/Avatar.d.ts vendored Normal file
View File

@ -0,0 +1,66 @@
import { VNode } from 'vue';
import { ClassComponent, GlobalComponentConstructor } from '../ts-helpers';
type AvatarSizeType = 'normal' | 'large' | 'xlarge' | undefined;
type AvatarShapeType = 'square' | 'circle' | undefined;
export interface AvatarProps {
/**
* Defines the text to display.
*/
label?: string | undefined;
/**
* Defines the icon to display.
*/
icon?: string | undefined;
/**
* Defines the image to display.
*/
image?: string | undefined;
/**
* Size of the element, valid options are 'normal', 'large' and 'xlarge'.
* @see AvatarSizeType
* Default value is 'normal'.
*/
size?: AvatarSizeType;
/**
* Shape of the element, valid options are 'square' and 'circle'.
* @see AvatarShapeType
* Default value is 'square'.
*/
shape?: AvatarShapeType;
}
export interface AvatarSlots {
/**
* Content can easily be customized with the default slot instead of using the built-in modes.
*/
default: () => VNode[];
}
export declare type AvatarEmits = {
/**
* Triggered when an error occurs while loading an image file.
*/
error: () => void;
}
declare class Avatar extends ClassComponent<AvatarProps, AvatarSlots, AvatarEmits> { }
declare module '@vue/runtime-core' {
interface GlobalComponents {
Avatar: GlobalComponentConstructor<Avatar>
}
}
/**
*
* Avatar represents people using icons, labels and images.
*
* Demos:
*
* - [Avatar](https://www.primefaces.org/primevue/showcase/#/avatar)
*
*/
export default Avatar;

View File

@ -0,0 +1,20 @@
import { mount } from '@vue/test-utils';
import Avatar from './Avatar.vue';
describe('Avatar.vue', () => {
it('should exist', () => {
const wrapper = mount(Avatar, {
props: {
label: 'T',
size: 'large',
shape: 'circle'
}
});
expect(wrapper.find('.p-avatar.p-component').exists()).toBe(true);
expect(wrapper.find('.p-avatar-lg').exists()).toBe(true);
expect(wrapper.find('.p-avatar-circle').exists()).toBe(true);
expect(wrapper.find('.p-avatar-text').exists()).toBe(true);
expect(wrapper.find('.p-avatar-text').text()).toBe('T');
});
});

View File

@ -0,0 +1,87 @@
<template>
<div :class="containerClass">
<slot>
<span class="p-avatar-text" v-if="label">{{label}}</span>
<span :class="iconClass" v-else-if="icon"></span>
<img :src="image" v-else-if="image" @error="onError">
</slot>
</div>
</template>
<script>
export default {
name: 'Avatar',
props: {
label: {
type: String,
default: null
},
icon: {
type: String,
default: null
},
image: {
type: String,
default: null
},
size: {
type: String,
default: 'normal'
},
shape: {
type: String,
default: "square"
}
},
methods: {
onError() {
this.$emit('error');
}
},
computed: {
containerClass() {
return ['p-avatar p-component', {
'p-avatar-image': this.image != null,
'p-avatar-circle': this.shape === 'circle',
'p-avatar-lg': this.size === 'large',
'p-avatar-xl': this.size === 'xlarge'
}];
},
iconClass() {
return ['p-avatar-icon', this.icon];
}
}
}
</script>
<style>
.p-avatar {
display: inline-flex;
align-items: center;
justify-content: center;
width: 2rem;
height: 2rem;
font-size: 1rem;
}
.p-avatar.p-avatar-image {
background-color: transparent;
}
.p-avatar.p-avatar-circle {
border-radius: 50%;
}
.p-avatar-circle img {
border-radius: 50%;
}
.p-avatar .p-avatar-icon {
font-size: 1rem;
}
.p-avatar img {
width: 100%;
height: 100%;
}
</style>

View File

@ -0,0 +1,9 @@
{
"main": "./avatar.cjs.js",
"module": "./avatar.esm.js",
"unpkg": "./avatar.min.js",
"types": "./Avatar.d.ts",
"browser": {
"./sfc": "./Avatar.vue"
}
}

33
components/avatargroup/AvatarGroup.d.ts vendored Normal file
View File

@ -0,0 +1,33 @@
import { ClassComponent, GlobalComponentConstructor } from '../ts-helpers';
export interface AvatarGroupProps {
}
export interface AvatarGroupSlots {
}
export declare type AvatarGroupEmits = {
}
declare class AvatarGroup extends ClassComponent<AvatarGroupProps, AvatarGroupSlots, AvatarGroupEmits> { }
declare module '@vue/runtime-core' {
interface GlobalComponents {
AvatarGroup: GlobalComponentConstructor<AvatarGroup>
}
}
/**
*
* A set of Avatars can be displayed together using the AvatarGroup component.
*
* Helper Components:
*
* - Avatar
*
* Demos:
*
* - [AvatarGroup](https://www.primefaces.org/primevue/showcase/#/avatar)
*
*/
export default AvatarGroup;

View File

@ -0,0 +1,21 @@
import { mount } from '@vue/test-utils';
import AvatarGroup from './AvatarGroup.vue';
import Avatar from '@/components/avatar/Avatar.vue';
describe('AvatarGroup.vue', () => {
it('should exist', () => {
const wrapper = mount(AvatarGroup, {
global: {
components: {
Avatar
}
},
slots: {
default: '<Avatar icon="pi pi-user" size="xlarge" shape="circle" />'
}
});
expect(wrapper.find('.p-avatar-group.p-component').exists()).toBe(true);
expect(wrapper.find('.p-avatar.p-component').exists()).toBe(true);
});
});

View File

@ -0,0 +1,22 @@
<template>
<div class="p-avatar-group p-component">
<slot></slot>
</div>
</template>
<script>
export default {
name: 'AvatarGroup'
}
</script>
<style>
.p-avatar-group .p-avatar + .p-avatar {
margin-left: -1rem;
}
.p-avatar-group {
display: flex;
align-items: center;
}
</style>

View File

@ -0,0 +1,9 @@
{
"main": "./avatargroup.cjs.js",
"module": "./avatargroup.esm.js",
"unpkg": "./avatargroup.min.js",
"types": "./AvatarGroup.d.ts",
"browser": {
"./sfc": "./AvatarGroup.vue"
}
}

View File

@ -0,0 +1,32 @@
.p-badge {
display: inline-block;
border-radius: 10px;
text-align: center;
padding: 0 .5rem;
}
.p-overlay-badge {
position: relative;
}
.p-overlay-badge .p-badge {
position: absolute;
top: 0;
right: 0;
transform: translate(50%,-50%);
transform-origin: 100% 0;
margin: 0;
}
.p-badge-dot {
width: .5rem;
min-width: .5rem;
height: .5rem;
border-radius: 50%;
padding: 0;
}
.p-badge-no-gutter {
padding: 0;
border-radius: 50%;
}

52
components/badge/Badge.d.ts vendored Normal file
View File

@ -0,0 +1,52 @@
import { VNode } from 'vue';
import { ClassComponent, GlobalComponentConstructor } from '../ts-helpers';
type BadgeSeverityType = 'info' | 'success' | 'warning' | 'danger' | undefined;
type BadgeSizeType = 'large' | 'xlarge' | undefined;
export interface BadgeProps {
/**
* Value to display inside the badge.
*/
value?: any;
/**
* Severity type of the badge.
* @see BadgeSeverityType
*/
severity?: BadgeSeverityType;
/**
* Size of the badge, valid options are 'large' and 'xlarge'.
* @see BadgeSizeType
*/
size?: BadgeSizeType;
}
export interface BadgeSlots {
/**
* Content can easily be customized with the default slot instead of using the built-in display.
*/
default: () => VNode[];
}
export declare type BadgeEmits = {
}
declare class Badge extends ClassComponent<BadgeProps, BadgeSlots, BadgeEmits> { }
declare module '@vue/runtime-core' {
interface GlobalComponents {
Badge: GlobalComponentConstructor<Badge>
}
}
/**
*
* Badge is a small status indicator for another element.
*
* Demos:
*
* - [Badge](https://www.primefaces.org/primevue/showcase/#/badge)
*
*/
export default Badge;

View File

@ -0,0 +1,33 @@
import { mount } from '@vue/test-utils';
import Badge from './Badge.vue';
import Button from '@/components/button/Button.vue';
describe('Badge.vue', () => {
it('should exist', () => {
const wrapper = mount(Badge, {
props: {
value: '29',
severity: 'warning',
size: 'large'
}
});
expect(wrapper.find('.p-badge.p-component').exists()).toBe(true);
expect(wrapper.find('.p-badge-warning').exists()).toBe(true);
expect(wrapper.find('.p-badge-lg').exists()).toBe(true);
expect(wrapper.find('.p-badge-no-gutter').exists()).toBe(false);
});
it('badge classes should exist', () => {
const wrapper = mount({
template: '<Button type="button" label="Messages" icon="pi pi-users" class="p-button-warning" badge="8" badgeClass="p-badge-danger" />',
components: {
Button
}
});
expect(wrapper.find('.p-badge.p-component').exists()).toBe(true);
expect(wrapper.find('.p-badge-danger').exists()).toBe(true);
expect(wrapper.find('.p-badge-no-gutter').exists()).toBe(true);
});
});

View File

@ -0,0 +1,33 @@
<template>
<span :class="badgeClass">
<slot>{{value}}</slot>
</span>
</template>
<script>
export default {
name: 'Badge',
props: {
value: null,
severity: null,
size: null
},
computed: {
containerClass() {
return this.$slots.default ? 'p-overlay-badge': this.badgeClass;
},
badgeClass() {
return ['p-badge p-component', {
'p-badge-no-gutter': this.value && String(this.value).length === 1,
'p-badge-dot': !this.value && !this.$slots.default,
'p-badge-lg': this.size === 'large',
'p-badge-xl': this.size === 'xlarge',
'p-badge-info': this.severity === 'info',
'p-badge-success': this.severity === 'success',
'p-badge-warning': this.severity === 'warning',
'p-badge-danger': this.severity === 'danger'
}];
}
}
}
</script>

View File

@ -0,0 +1,9 @@
{
"main": "./badge.cjs.js",
"module": "./badge.esm.js",
"unpkg": "./badge.min.js",
"types": "./Badge.d.ts",
"browser": {
"./sfc": "./Badge.vue"
}
}

View File

@ -0,0 +1,5 @@
import { ObjectDirective } from 'vue';
declare const BadgeDirective: ObjectDirective;
export default BadgeDirective;

View File

@ -0,0 +1,57 @@
import {DomHandler} from 'primevue/utils';
import {UniqueComponentId} from 'primevue/utils';
const BadgeDirective = {
beforeMount(el, options) {
const id = UniqueComponentId() + '_badge';
el.$_pbadgeId = id;
let badge = document.createElement('span');
badge.id = id;
badge.className = 'p-badge p-component';
for (let modifier in options.modifiers) {
DomHandler.addClass(badge, 'p-badge-' + modifier);
}
if (options.value != null) {
badge.appendChild(document.createTextNode(options.value));
if (String(options.value).length === 1) {
DomHandler.addClass(badge, 'p-badge-no-gutter');
}
}
else {
DomHandler.addClass(badge, 'p-badge-dot');
}
DomHandler.addClass(el, 'p-overlay-badge');
el.appendChild(badge);
},
updated(el, options) {
DomHandler.addClass(el, 'p-overlay-badge');
if (options.oldValue !== options.value) {
let badge = document.getElementById(el.$_pbadgeId);
if (options.value) {
if (DomHandler.hasClass(badge, 'p-badge-dot')) {
DomHandler.removeClass(badge, 'p-badge-dot');
}
if (String(options.value).length === 1)
DomHandler.addClass(badge, 'p-badge-no-gutter');
else
DomHandler.removeClass(badge, 'p-badge-no-gutter');
}
else if (!options.value && !DomHandler.hasClass(badge, 'p-badge-dot')) {
DomHandler.addClass(badge, 'p-badge-dot');
}
badge.innerHTML = '';
badge.appendChild(document.createTextNode(options.value));
}
}
};
export default BadgeDirective;

View File

@ -0,0 +1,17 @@
import { mount } from '@vue/test-utils';
import BadgeDirective from './BadgeDirective';
describe('directive badge should exist', () => {
it('positioned badge', () => {
const wrapper = mount({
template: '<i class="pi pi-bell mr-4 p-text-secondary" style="font-size: 2rem" v-badge="2"></i>',
directives: {
'badge': BadgeDirective
}
});
expect(wrapper.find('.p-overlay-badge').exists()).toBe(true);
expect(wrapper.find('.p-badge.p-component').exists()).toBe(true);
expect(wrapper.find('.p-badge.p-component').text()).toBe('2');
});
});

View File

@ -0,0 +1,6 @@
{
"main": "./badgedirective.cjs.js",
"module": "./badgedirective.esm.js",
"unpkg": "./badgedirective.min.js",
"types": "./BadgeDirective.d.ts"
}

60
components/blockui/BlockUI.d.ts vendored Executable file
View File

@ -0,0 +1,60 @@
import { VNode } from 'vue';
import { ClassComponent, GlobalComponentConstructor } from '../ts-helpers';
export interface BlockUIProps {
/**
* Controls the blocked state.
*/
blocked?: boolean | undefined;
/**
* When enabled, the whole document gets blocked.
*/
fullScreen?: boolean | undefined;
/**
* Base zIndex value to use in layering.
* Default value is 0.
*/
baseZIndex?: number | undefined;
/**
* Whether to automatically manage layering.
* Default value is true.
*/
autoZIndex?: boolean | undefined;
}
export interface BlockUISlots {
/**
* Custom content's slot.
*/
default: () => VNode[];
}
export declare type BlockUIEmits = {
/**
* Fired when the element gets blocked.
*/
'block': () => void;
/**
* Fired when the element gets unblocked.
*/
'unblock': () => void;
}
declare class BlockUI extends ClassComponent<BlockUIProps, BlockUISlots, BlockUIEmits> { }
declare module '@vue/runtime-core' {
interface GlobalComponents {
BlockUI: GlobalComponentConstructor<BlockUI>
}
}
/**
*
* BlockUI can either block other components or the whole page.
*
* Demos:
*
* - [BlockUI](https://www.primefaces.org/primevue/showcase/#/blockui)
*
*/
export default BlockUI;

View File

@ -0,0 +1,63 @@
import { config, mount } from '@vue/test-utils';
import BlockUI from './BlockUI.vue';
import Panel from '@/components/panel/Panel.vue';
import Button from '@/components/button/Button.vue';
config.global.mocks = {
$primevue: {
config: {
zIndex: {
modal: 1100
}
}
}
}
describe('BlockUI.vue', () => {
it('should blocked and unblocked the panel', async () => {
const wrapper = mount({
template: `
<Button type="button" label="Block" @click="blockPanel()"></Button>
<Button type="button" label="Unblock" @click="unblockPanel()"></Button>
<BlockUI :blocked="blockedPanel">
<Panel header="Godfather I" style="margin-top: 20px">
<p>The story begins as Don Vito Corleone, the head of a New York Mafia family.</p>
</Panel>
</BlockUI>
`,
components: {
BlockUI,
Panel,
Button
},
data() {
return {
blockedPanel: false
}
},
methods: {
blockPanel() {
this.blockedPanel = true;
},
unblockPanel() {
this.blockedPanel = false;
}
}
});
expect(wrapper.find('.p-blockui-container').exists()).toBe(true);
const buttons = wrapper.findAll('.p-button');
await buttons[0].trigger('click');
expect(wrapper.find('.p-blockui').exists()).toBe(true);
expect(wrapper.find('.p-blockui').classes()).toContain('p-component-overlay-enter');
expect(wrapper.find('.p-blockui').attributes().style).toEqual('z-index: 1101;');
await buttons[1].trigger('click');
expect(wrapper.find('.p-blockui').classes()).toContain('p-component-overlay-leave');
});
});

110
components/blockui/BlockUI.vue Executable file
View File

@ -0,0 +1,110 @@
<template>
<div ref="container" class="p-blockui-container" v-bind="$attrs">
<slot></slot>
</div>
</template>
<script>
import {DomHandler,ZIndexUtils} from 'primevue/utils';
export default {
name: 'BlockUI',
emits: ['block', 'unblock'],
props: {
blocked: {
type: Boolean,
default: false
},
fullScreen: {
type: Boolean,
default: false
},
baseZIndex: {
type: Number,
default: 0
},
autoZIndex: {
type: Boolean,
default: true
}
},
mask: null,
mounted() {
if (this.blocked) {
this.block();
}
},
watch: {
blocked(newValue) {
if (newValue === true)
this.block();
else
this.unblock();
}
},
methods: {
block() {
let styleClass = 'p-blockui p-component-overlay p-component-overlay-enter';
if (this.fullScreen) {
styleClass += ' p-blockui-document';
this.mask = document.createElement('div');
this.mask.setAttribute('class', styleClass);
document.body.appendChild(this.mask);
DomHandler.addClass(document.body, 'p-overflow-hidden');
document.activeElement.blur();
}
else {
this.mask = document.createElement('div');
this.mask.setAttribute('class', styleClass);
this.$refs.container.appendChild(this.mask);
}
if (this.autoZIndex) {
ZIndexUtils.set('modal', this.mask, this.baseZIndex + this.$primevue.config.zIndex.modal);
}
this.$emit('block');
},
unblock() {
DomHandler.addClass(this.mask, 'p-component-overlay-leave');
this.mask.addEventListener('animationend', () => {
this.removeMask();
});
},
removeMask() {
ZIndexUtils.clear(this.mask);
if (this.fullScreen) {
document.body.removeChild(this.mask);
DomHandler.removeClass(document.body, 'p-overflow-hidden');
}
else {
this.$refs.container.removeChild(this.mask);
}
this.$emit('unblock');
}
}
}
</script>
<style>
.p-blockui-container {
position: relative;
}
.p-blockui {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
.p-blockui.p-component-overlay {
position: absolute;
}
.p-blockui-document.p-component-overlay {
position: fixed;
}
</style>

View File

@ -0,0 +1,9 @@
{
"main": "./blockui.cjs.js",
"module": "./blockui.esm.js",
"unpkg": "./blockui.min.js",
"types": "./BlockUI.d.ts",
"browser": {
"./sfc": "./BlockUI.vue"
}
}

58
components/breadcrumb/Breadcrumb.d.ts vendored Executable file
View File

@ -0,0 +1,58 @@
import { VNode } from 'vue';
import { ClassComponent, GlobalComponentConstructor } from '../ts-helpers';
import { MenuItem } from '../menuitem';
export interface BreadcrumbProps {
/**
* An array of menuitems.
*/
model?: MenuItem[];
/**
* Configuration for the home icon.
*/
home?: MenuItem;
/**
* Whether to apply 'router-link-active-exact' class if route exactly matches the item path.
* Default value is true.
*/
exact?: boolean;
}
export interface BreadcrumbSlots {
/**
* Custom item template.
* @param {Object} scope - item slot's params.
*/
item: (scope: {
/**
* Menuitem instance
*/
item: MenuItem;
}) => VNode[];
}
export declare type BreadcrumbEmits = {
}
declare class Breadcrumb extends ClassComponent<BreadcrumbProps, BreadcrumbSlots, BreadcrumbEmits> { }
declare module '@vue/runtime-core' {
interface GlobalComponents {
Breadcrumb: GlobalComponentConstructor<Breadcrumb>
}
}
/**
*
* Breadcrumb provides contextual information about page hierarchy.
*
* Helper API:
*
* - [MenuItem](https://www.primefaces.org/primevue/showcase/#/menumodel)
*
* Demos:
*
* - [Breadcrumb](https://www.primefaces.org/primevue/showcase/#/breadcrumb)
*
*/
export default Breadcrumb;

View File

@ -0,0 +1,26 @@
import { mount } from '@vue/test-utils';
import Breadcrumb from './Breadcrumb.vue';
describe('Breadcrumb', () => {
it('should exist', () => {
const wrapper = mount(Breadcrumb, {
global: {
stubs: ['router-link']
},
props: {
home: {icon: 'pi pi-home', to: '/'},
model: [
{label: 'Computer'},
{label: 'Notebook'},
{label: 'Accessories'},
{label: 'Backpacks'},
{label: 'Item'}
]
}
});
expect(wrapper.find('.p-breadcrumb.p-component').exists()).toBe(true);
expect(wrapper.findAll('.p-breadcrumb-chevron').length).toBe(5);
expect(wrapper.findAll('.p-menuitem-text').length).toBe(5);
});
});

View File

@ -0,0 +1,63 @@
<template>
<nav class="p-breadcrumb p-component" aria-label="Breadcrumb">
<ul>
<BreadcrumbItem v-if="home" :item="home" class="p-breadcrumb-home" :template="$slots.item" :exact="exact" />
<template v-for="item of model" :key="item.label">
<li class="p-breadcrumb-chevron pi pi-chevron-right"></li>
<BreadcrumbItem :item="item" :template="$slots.item" :exact="exact" />
</template>
</ul>
</nav>
</template>
<script>
import BreadcrumbItem from './BreadcrumbItem.vue';
export default {
name: 'Breadcrumb',
props: {
model: {
type: Array,
default: null
},
home: {
type: null,
default: null
},
exact: {
type: Boolean,
default: true
}
},
components: {
'BreadcrumbItem': BreadcrumbItem
}
}
</script>
<style>
.p-breadcrumb {
overflow-x: auto;
}
.p-breadcrumb ul {
margin: 0;
padding: 0;
list-style-type: none;
display: flex;
align-items: center;
flex-wrap: nowrap;
}
.p-breadcrumb .p-menuitem-text {
line-height: 1;
}
.p-breadcrumb .p-menuitem-link {
text-decoration: none;
}
.p-breadcrumb::-webkit-scrollbar {
display: none;
}
</style>

View File

@ -0,0 +1,65 @@
<template>
<li :class="containerClass(item)" v-if="visible()">
<template v-if="!template">
<router-link v-if="item.to" :to="item.to" custom v-slot="{navigate, href, isActive, isExactActive}">
<a :href="href" :class="linkClass({isActive, isExactActive})" @click="onClick($event, navigate)">
<span v-if="item.icon" :class="iconClass"></span>
<span v-if="item.label" class="p-menuitem-text">{{label()}}</span>
</a>
</router-link>
<a v-else :href="item.url||'#'" :class="linkClass()" @click="onClick" :target="item.target">
<span v-if="item.icon" :class="iconClass"></span>
<span v-if="item.label" class="p-menuitem-text">{{label()}}</span>
</a>
</template>
<component v-else :is="template" :item="item"></component>
</li>
</template>
<script>
export default {
name: 'BreadcrumbItem',
props: {
item: null,
template: null,
exact: null
},
methods: {
onClick(event, navigate) {
if (this.item.command) {
this.item.command({
originalEvent: event,
item: this.item
});
}
if (this.item.to && navigate) {
navigate(event);
}
},
containerClass(item) {
return [{'p-disabled': this.disabled(item)}, this.item.class];
},
linkClass(routerProps) {
return ['p-menuitem-link', {
'router-link-active': routerProps && routerProps.isActive,
'router-link-active-exact': this.exact && routerProps && routerProps.isExactActive
}];
},
visible() {
return (typeof this.item.visible === 'function' ? this.item.visible() : this.item.visible !== false);
},
disabled(item) {
return (typeof item.disabled === 'function' ? item.disabled() : item.disabled);
},
label() {
return (typeof this.item.label === 'function' ? this.item.label() : this.item.label);
}
},
computed: {
iconClass() {
return ['p-menuitem-icon', this.item.icon];
}
}
}
</script>

View File

@ -0,0 +1,10 @@
{
"main": "./breadcrumb.cjs.js",
"module": "./breadcrumb.esm.js",
"unpkg": "./breadcrumb.min.js",
"types": "./Breadcrumb.d.ts"
,
"browser": {
"./sfc": "./Breadcrumb.vue"
}
}

68
components/button/Button.css Executable file
View File

@ -0,0 +1,68 @@
.p-button {
margin: 0;
display: inline-flex;
cursor: pointer;
user-select: none;
align-items: center;
vertical-align: bottom;
text-align: center;
overflow: hidden;
position: relative;
}
.p-button-label {
flex: 1 1 auto;
}
.p-button-icon-right {
order: 1;
}
.p-button:disabled {
cursor: default;
}
.p-button-icon-only {
justify-content: center;
}
.p-button-icon-only .p-button-label {
visibility: hidden;
width: 0;
flex: 0 0 auto;
}
.p-button-vertical {
flex-direction: column;
}
.p-button-icon-bottom {
order: 2;
}
.p-buttonset .p-button {
margin: 0;
}
.p-buttonset .p-button:not(:last-child) {
border-right: 0 none;
}
.p-buttonset .p-button:not(:first-of-type):not(:last-of-type) {
border-radius: 0;
}
.p-buttonset .p-button:first-of-type {
border-top-right-radius: 0;
border-bottom-right-radius: 0;
}
.p-buttonset .p-button:last-of-type {
border-top-left-radius: 0;
border-bottom-left-radius: 0;
}
.p-buttonset .p-button:focus {
position: relative;
z-index: 1;
}

74
components/button/Button.d.ts vendored Executable file
View File

@ -0,0 +1,74 @@
import { ButtonHTMLAttributes, VNode } from 'vue';
import { ClassComponent, GlobalComponentConstructor } from '../ts-helpers';
type ButtonIconPosType = 'left' | 'right' | 'top' | 'bottom' | undefined;
export interface ButtonProps extends ButtonHTMLAttributes {
/**
* Inline style of the button.
*/
style?: any;
/**
* Style class of the button.
*/
class?: any;
/**
* Text of the button.
*/
label?: string | undefined;
/**
* Name of the icon.
*/
icon?: string | undefined;
/**
* Position of the icon, valid values are 'left', 'right', 'bottom' and 'top'.
* Default value is 'left'.
*/
iconPos?: ButtonIconPosType;
/**
* Value of the badge.
*/
badge?: string | undefined;
/**
* Style class of the badge.
*/
badgeClass?: string | undefined;
/**
* Whether the button is in loading state.
*/
loading?: boolean | undefined;
/**
* Icon to display in loading state.
* Default value is 'pi pi-spinner pi-spin'.
*/
loadingIcon?: string | undefined;
}
export interface ButtonSlots {
/**
* Custom content such as icons, images and text can be placed inside the button via the default slot. Note that when slot is used, label, icon and badge properties are not included.
*/
default: () => VNode[];
}
export declare type ButtonEmits = {
}
declare class Button extends ClassComponent<ButtonProps, ButtonSlots, ButtonEmits> { }
declare module '@vue/runtime-core' {
interface GlobalComponents {
Button: GlobalComponentConstructor<Button>
}
}
/**
*
* Button is an extension to standard button element with icons and theming.
*
* Demos:
*
* - [Button](https://www.primefaces.org/primevue/showcase/#/button)
*
*/
export default Button;

View File

@ -0,0 +1,85 @@
import { h } from 'vue';
import { mount } from '@vue/test-utils';
import Button from '@/components/button/Button.vue';
describe('Button.vue', () => {
it('is Button element exist', () => {
const wrapper = mount(Button);
expect(wrapper.find('.p-button.p-component').exists()).toBe(true);
expect(wrapper.find('.p-button-label').exists()).toBe(true);
})
});
describe('Button.vue', () => {
it('is icon exist and right position', () => {
const icon = 'pi pi-discord';
const iconPos = 'right';
const label = 'Save';
const props = { icon, iconPos };
let wrapper;
wrapper = mount(Button, {
props
});
expect(wrapper.find('.p-button-icon-only').exists()).toBe(true);
wrapper = mount(Button, {
props: { ...props, label }
});
expect(wrapper.find('.p-button-icon.p-button-icon-' + iconPos).exists()).toBe(true);
})
});
describe('Button.vue', () => {
it('is badge working', () => {
const badge = '5';
const badgeClass= 'p-badge-danger';
const wrapper = mount(Button, {
props: { badge, badgeClass }
});
expect(wrapper.find('.p-badge').text()).toEqual(badge);
expect(wrapper.find('.' + badgeClass).exists()).toBe(true);
})
});
describe('Button.vue', () => {
it('is loading working', async () => {
const loadingIcon = 'pi pi-discord';
const wrapper = mount(Button, {
props: {
loading: false,
loadingIcon
}
});
expect(wrapper.find('.p-disabled').exists()).toBe(false);
await wrapper.setProps({ loading: true })
const array = loadingIcon.split(' ');
const lastIcon = '.' + array.join('.');
expect(wrapper.find('.p-button-loading').exists()).toBe(true);
expect(wrapper.find('.p-button-loading-icon' + lastIcon).exists()).toBe(true);
await wrapper.setProps({ loading: false });
expect(wrapper.find('.p-button-loading').exists()).toBe(false);
})
});
describe('Button.vue', () => {
it('should render default slot', () => {
const wrapper = mount(Button, {
slots: {
default: h('span', {class: 'ml-2 font-bold'}, 'Default PrimeVue Button')
}
});
expect(wrapper.html()).toBe('<button class="p-button p-component" type="button"><span class="ml-2 font-bold">Default PrimeVue Button</span></button>');
})
});

84
components/button/Button.vue Executable file
View File

@ -0,0 +1,84 @@
<template>
<button :class="buttonClass" type="button" :aria-label="defaultAriaLabel" v-ripple :disabled="disabled">
<slot>
<span v-if="loading && !icon" :class="iconClass"></span>
<span v-if="icon" :class="iconClass"></span>
<span class="p-button-label">{{label||'&nbsp;'}}</span>
<span v-if="badge" :class="badgeStyleClass">{{badge}}</span>
</slot>
</button>
</template>
<script>
import Ripple from 'primevue/ripple';
export default {
name: 'Button',
props: {
label: {
type: String
},
icon: {
type: String
},
iconPos: {
type: String,
default: 'left'
},
badge: {
type: String
},
badgeClass: {
type: String,
default: null
},
loading: {
type: Boolean,
default: false
},
loadingIcon: {
type: String,
default: 'pi pi-spinner pi-spin'
}
},
computed: {
buttonClass() {
return {
'p-button p-component': true,
'p-button-icon-only': this.icon && !this.label,
'p-button-vertical': (this.iconPos === 'top' || this.iconPos === 'bottom') && this.label,
'p-disabled': this.$attrs.disabled || this.loading,
'p-button-loading': this.loading,
'p-button-loading-label-only': this.loading && !this.icon && this.label
}
},
iconClass() {
return [
this.loading ? 'p-button-loading-icon ' + this.loadingIcon : this.icon,
'p-button-icon',
{
'p-button-icon-left': this.iconPos === 'left' && this.label,
'p-button-icon-right': this.iconPos === 'right' && this.label,
'p-button-icon-top': this.iconPos === 'top' && this.label,
'p-button-icon-bottom': this.iconPos === 'bottom' && this.label
}
]
},
badgeStyleClass() {
return [
'p-badge p-component', this.badgeClass, {
'p-badge-no-gutter': this.badge && String(this.badge).length === 1
}]
},
disabled() {
return this.$attrs.disabled || this.loading;
},
defaultAriaLabel() {
return (this.label ? this.label + (this.badge ? ' ' + this.badge : '') : this.$attrs['aria-label']);
}
},
directives: {
'ripple': Ripple
}
}
</script>

View File

@ -0,0 +1,9 @@
{
"main": "./button.cjs.js",
"module": "./button.esm.js",
"unpkg": "./button.min.js",
"types": "./Button.d.ts",
"browser": {
"./sfc": "./Button.vue"
}
}

400
components/calendar/Calendar.d.ts vendored Executable file
View File

@ -0,0 +1,400 @@
import { HTMLAttributes, InputHTMLAttributes, VNode } from 'vue';
import { ClassComponent, GlobalComponentConstructor } from '../ts-helpers';
type CalendarValueType = Date | Date[] | undefined;
type CalendarSlotDateType = { day: number; month: number; year: number; today: boolean; selectable: boolean }
type CalendarSelectionModeType = 'single' | 'multiple' | 'range' | undefined;
type CalendarViewType = 'date' | 'month' | 'year' | undefined;
type CalendarHourFormatType = '12' | '24' | undefined;
type CalendarAppendToType = 'body' | 'self' | string | undefined | HTMLElement;
export interface CalendarResponsiveOptions {
/**
* Breakpoint for responsive mode. Exp; @media screen and (max-width: ${breakpoint}) {...}
*/
breakpoint: string;
/**
* The number of visible months on breakpoint.
*/
numMonths: number;
}
export interface CalendarMonthChangeEvent {
/**
* New month.
*/
month: number;
/**
* New year.
*/
year: number;
}
export interface CalendarYearChangeEvent {
/**
* New month.
*/
month: number;
/**
* New year.
*/
year: number;
}
export interface CalendarBlurEvent {
/**
* Browser event
*/
originalEvent: Event;
/**
* Input value
*/
value: string;
}
export interface CalendarProps {
/**
* Value of the component.
*/
modelValue?: CalendarValueType;
/**
* Defines the quantity of the selection, valid values are 'single', 'multiple' and 'range'.
* @see CalendarSelectionModeType
* Default value is 'single'.
*/
selectionMode?: CalendarSelectionModeType;
/**
* Format of the date. Defaults to PrimeVue Locale configuration.
*/
dateFormat?: string | undefined;
/**
* When enabled, displays the calendar as inline instead of an overlay.
*/
inline?: boolean | undefined;
/**
* Whether to display dates in other months (non-selectable) at the start or end of the current month. To make these days selectable use the selectOtherMonths option.
* Default value is true.
*/
showOtherMonths?: boolean | undefined;
/**
* Whether days in other months shown before or after the current month are selectable. This only applies if the showOtherMonths option is set to true.
*/
selectOtherMonths?: boolean | undefined;
/**
* When enabled, displays a button with icon next to input.
*/
showIcon?: boolean | undefined;
/**
* Icon of the calendar button.
* Default value is 'pi pi-calendar'.
*/
icon?: string | undefined;
/**
* Number of months to display.
* Default value is 1.
*/
numberOfMonths?: number | undefined;
/**
* An array of options for responsive design.
* @see CalendarResponsiveOptions
*/
responsiveOptions?: CalendarResponsiveOptions[];
/**
* Type of view to display, valid values are 'date', 'month' and 'year'.
* @see CalendarViewType
* Default value is 'date'.
*/
view?: CalendarViewType;
/**
* When enabled, calendar overlay is displayed as optimized for touch devices.
*/
touchUI?: boolean | undefined;
/**
* Whether the month should be rendered as a dropdown instead of text.
*
* @deprecated since version 3.9.0, Navigator is always on.
*/
monthNavigator?: boolean | undefined;
/**
* Whether the year should be rendered as a dropdown instead of text.
*
* @deprecated since version 3.9.0, Navigator is always on.
*/
yearNavigator?: boolean | undefined;
/**
* The range of years displayed in the year drop-down in (nnnn:nnnn) format such as (2000:2020).
*
* @deprecated since version 3.9.0, Years are based on decades by default.
*/
yearRange?: string | undefined;
/**
* The minimum selectable date.
*/
minDate?: Date | undefined;
/**
* The maximum selectable date.
*/
maxDate?: Date | undefined;
/**
* Array with dates to disable.
*/
disabledDates?: Date[] | undefined;
/**
* Array with disabled weekday numbers.
*/
disabledDays?: number[] | undefined;
/**
* Maximum number of selectable dates in multiple mode.
*/
maxDateCount?: number | undefined;
/**
* When disabled, datepicker will not be visible with input focus.
* Default value is true.
*/
showOnFocus?: boolean | undefined;
/**
* Whether to automatically manage layering.
* Default value is true.
*/
autoZIndex?: boolean | undefined;
/**
* Base zIndex value to use in layering.
* Default value is 0.
*/
baseZIndex?: number | undefined;
/**
* Whether to display today and clear buttons at the footer.
*/
showButtonBar?: boolean | undefined;
/**
* The cutoff year for determining the century for a date.
* Default value is +10.
*/
shortYearCutoff?: string | undefined;
/**
* Whether to display timepicker.
*/
showTime?: boolean | undefined;
/**
* Whether to display timepicker only.
*/
timeOnly?: boolean | undefined;
/**
* Specifies 12 or 24 hour format.
*/
hourFormat?: CalendarHourFormatType;
/**
* Hours to change per step.
* Default value is 1.
*/
stepHour?: number | undefined;
/**
* Minutes to change per step.
* Default value is 1.
*/
stepMinute?: number | undefined;
/**
* Seconds to change per step.
* Default value is 1.
*/
stepSecond?: number | undefined;
/**
* Whether to show the seconds in time picker.
*/
showSeconds?: boolean | undefined;
/**
* Whether to hide the overlay on date selection when showTime is enabled.
*/
hideOnDateTimeSelect?: boolean | undefined;
/**
* Whether to hide the overlay on date selection is completed when selectionMode is range.
*/
hideOnRangeSelection?: boolean | undefined;
/**
* Separator of time selector.
* Default value is ':'.
*/
timeSeparator?: string | undefined;
/**
* When enabled, calendar will show week numbers.
*/
showWeek?: boolean | undefined;
/**
* Wheter to allow prevents entering the date manually via typing.
* Default value is true.
*/
manualInput?: boolean | undefined;
/**
* When present, it specifies that the component should be disabled.
*/
disabled?: boolean | undefined;
/**
* When present, it specifies that an input field is read-only.
*/
readonly?: boolean | undefined;
/**
* Placeholder text for the input.
*/
placeholder?: string | undefined;
/**
* A valid query selector or an HTMLElement to specify where the overlay gets attached. Special keywords are 'body' for document body and 'self' for the element itself.
* @see CalendarAppendToType
* Default value is 'body'.
*/
appendTo?: CalendarAppendToType;
/**
* Identifier of the element.
*/
id?: string | undefined;
/**
* Identifier of the underlying input element.
*/
inputId?: string | undefined;
/**
* Inline style of the input field.
*/
inputStyle?: any | undefined;
/**
* Style class of the input field.
*/
inputClass?: any | undefined;
/**
* Uses to pass all properties of the HTMLInputElement to the focusable input element inside the component.
*/
inputProps?: InputHTMLAttributes | undefined;
/**
* Inline style of the overlay panel.
*/
panelStyle?: any | undefined;
/**
* Style class of the overlay panel.
*/
panelClass?: any | undefined;
/**
* Uses to pass all properties of the HTMLDivElement to the overlay panel inside the component.
*/
panelProps?: HTMLAttributes | undefined;
/**
* Establishes relationships between the component and label(s) where its value should be one or more element IDs.
*/
'aria-labelledby'?: string | undefined;
/**
* Establishes a string value that labels the component.
*/
'aria-label'?: string | undefined;
}
export interface CalendarSlots {
/**
* Custom header template of panel.
*/
header: () => VNode[];
/**
* Custom footer template of panel.
*/
footer: () => VNode[];
/**
* Custom date template.
* @param {Object} scope - date slot's params.
*/
date: (scope: {
/**
* Value of the component.
*/
date: CalendarSlotDateType;
}) => VNode[];
/**
* Custom decade template.
* @param {CalendarDecadeSlot} scope - decade slot's params.
*/
decade: (scope: {
/**
* An array containing the start and and year of a decade to display at header of the year picker.
*/
years: string[] | undefined;
}) => VNode[];
}
export declare type CalendarEmits = {
/**
* Emitted when the value changes.
* @param {CalendarValueType} value - New value.
*/
'update:modelValue': (value: CalendarValueType) => void;
/**
* Callback to invoke when input field is being typed.
* @param {Event} event - Browser event
*/
'input': (event: Event) => void;
/**
* Callback to invoke when a date is selected.
* @param {Date} value - Selected value.
*/
'date-select': (value: Date) => void;
/**
* Callback to invoke when datepicker panel is shown.
*/
'show': () => void;
/**
* Callback to invoke when datepicker panel is hidden.
*/
'hide': () => void;
/**
* Callback to invoke when today button is clicked.
* @param {Date} date - Today as a date instance.
*/
'today-click': (date: Date) => void;
/**
* Callback to invoke when clear button is clicked.
* @param {Event} event - Browser event.
*/
'clear-click': (event: Event) => void;
/**
* Callback to invoke when a month is changed using the navigators.
* @param {CalendarMonthChangeEvent} event - Custom month change event.
*/
'month-change': (event: CalendarMonthChangeEvent) => void;
/**
* Callback to invoke when a year is changed using the navigators.
* @param {CalendarYearChangeEvent} event - Custom year change event.
*/
'year-change': (event: CalendarYearChangeEvent) => void;
/**
* Callback to invoke on focus of input field.
* @param {Event} event - Focus event
*/
'focus': (event: Event) => void;
/**
* Callback to invoke on blur of input field.
* @param {CalendarBlurEvent} event - Blur event
*/
'blur': (event: CalendarBlurEvent) => void;
/**
* Callback to invoke when a key is pressed.
*/
'keydown': (event: Event) => void;
}
declare class Calendar extends ClassComponent<CalendarProps, CalendarSlots, CalendarEmits> { }
declare module '@vue/runtime-core' {
interface GlobalComponents {
Calendar: GlobalComponentConstructor<Calendar>
}
}
/**
*
* Calendar is an input component to select a date.
*
* Demos:
*
* - [Calendar](https://www.primefaces.org/primevue/showcase/#/calendar)
*
*/
export default Calendar;

View File

@ -0,0 +1,53 @@
import { mount } from '@vue/test-utils';
import PrimeVue from '@/components/config/PrimeVue';
import Calendar from './Calendar.vue';
describe('Calendar.vue', () => {
let wrapper;
beforeEach(() => {
wrapper = mount(Calendar, {
global: {
plugins: [PrimeVue],
stubs: {
teleport: true
}
},
props: {
modelValue: new Date()
}
});
});
it('should exist', async() => {
expect(wrapper.find('.p-calendar.p-component').exists()).toBe(true);
expect(wrapper.find('.p-inputtext').exists()).toBe(true);
let input = wrapper.find('.p-inputtext');
await input.trigger('focus');
expect(wrapper.find('.p-datepicker.p-component').exists()).toBe(true);
expect(wrapper.find('.p-datepicker-today').exists()).toBe(true);
expect(wrapper.find('.p-highlight').exists()).toBe(true);
expect(wrapper.find('.p-highlight').text()).toEqual(new Date().getDate().toString());
});
it('should select a date', async () => {
await wrapper.setProps({ inline: true });
const event = {day: 8, month: 2, year: 2022, today: false, selectable: true};
const onDateSelect = jest.spyOn(wrapper.vm, 'onDateSelect');
await wrapper.vm.onDateSelect({currentTarget: {focus: () => {}}}, event);
expect(onDateSelect).toHaveBeenCalled()
});
it('should calculate the correct view date when in range mode', async () => {
const dateOne = new Date();
const dateTwo = new Date();
dateTwo.setFullYear(dateOne.getFullYear(), dateOne.getMonth(), dateOne.getDate() + 1)
await wrapper.setProps({ selectionMode: 'range', showTime: true, modelValue: [dateOne, dateTwo] });
expect(wrapper.vm.viewDate).toEqual(dateTwo)
});
});

2815
components/calendar/Calendar.vue Executable file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,9 @@
{
"main": "./calendar.cjs.js",
"module": "./calendar.esm.js",
"unpkg": "./calendar.min.js",
"types": "./Calendar.d.ts",
"browser": {
"./sfc": "./Calendar.vue"
}
}

50
components/card/Card.d.ts vendored Executable file
View File

@ -0,0 +1,50 @@
import { VNode } from 'vue';
import { ClassComponent, GlobalComponentConstructor } from '../ts-helpers';
export interface CardProps {
}
export interface CardSlots {
/**
* Custom header template.
*/
header: () => VNode[];
/**
* Custom title template.
*/
title: () => VNode[];
/**
* Custom subtitle template.
*/
subtitle: () => VNode[];
/**
* Custom content template.
*/
content: () => VNode[];
/**
* Custom footer template.
*/
footer: () => VNode[];
}
export declare type CardEmits = {
}
declare class Card extends ClassComponent<CardProps, CardSlots, CardEmits> { }
declare module '@vue/runtime-core' {
interface GlobalComponents {
Card: GlobalComponentConstructor<Card>
}
}
/**
*
* Card is a flexible container component.
*
* Demos:
*
* - [Card](https://www.primefaces.org/primevue/showcase/#/card)
*
*/
export default Card;

View File

@ -0,0 +1,23 @@
import { mount } from '@vue/test-utils';
import Card from './Card.vue';
describe('Card.vue', () => {
it('should exist', () => {
const wrapper = mount(Card, {
slots: {
header: 'Advanced Card Header',
title: 'Advanced Card',
subtitle: 'Advanced subtitle',
content: '<p>Lorem ipsum dolor sit amet, consectetur adipisicing elit.</p>',
footer: 'Advanced Card Footer'
}
});
expect(wrapper.find('.p-card.p-component').exists()).toBe(true);
expect(wrapper.find('.p-card-header').text()).toBe('Advanced Card Header');
expect(wrapper.find('.p-card-title').text()).toBe('Advanced Card');
expect(wrapper.find('.p-card-subtitle').text()).toBe('Advanced subtitle');
expect(wrapper.find('.p-card-content').text()).toBe('Lorem ipsum dolor sit amet, consectetur adipisicing elit.');
expect(wrapper.find('.p-card-footer').text()).toBe('Advanced Card Footer');
});
});

29
components/card/Card.vue Executable file
View File

@ -0,0 +1,29 @@
<template>
<div class="p-card p-component">
<div class="p-card-header" v-if="$slots.header">
<slot name="header"></slot>
</div>
<div class="p-card-body">
<div class="p-card-title" v-if="$slots.title"><slot name="title"></slot></div>
<div class="p-card-subtitle" v-if="$slots.subtitle"><slot name="subtitle"></slot></div>
<div class="p-card-content">
<slot name="content"></slot>
</div>
<div class="p-card-footer" v-if="$slots.footer">
<slot name="footer"></slot>
</div>
</div>
</div>
</template>
<script>
export default {
name: 'Card'
}
</script>
<style>
.p-card-header img {
width: 100%;
}
</style>

View File

@ -0,0 +1,9 @@
{
"main": "./card.cjs.js",
"module": "./card.esm.js",
"unpkg": "./card.min.js",
"types": "./Card.d.ts",
"browser": {
"./sfc": "./Card.vue"
}
}

130
components/carousel/Carousel.d.ts vendored Executable file
View File

@ -0,0 +1,130 @@
import { VNode } from 'vue';
import { ClassComponent, GlobalComponentConstructor } from '../ts-helpers';
type CarouselOrientationType = 'horizontal' | 'vertical' | undefined;
export interface CarouselResponsiveOptions {
/**
* Breakpoint for responsive mode. Exp; @media screen and (max-width: ${breakpoint}) {...}
*/
breakpoint: string;
/**
* The number of visible items on breakpoint.
*/
numVisible: number;
/**
* The number of scrolled items on breakpoint.
*/
numScroll: number;
}
export interface CarouselProps {
/**
* An array of objects to display.
*/
value?: any | undefined;
/**
* Index of the first item.
* Default value is 0.
*/
page?: number | undefined;
/**
* Number of items per page.
* Default value is 1.
*/
numVisible?: number | undefined;
/**
* Number of items to scroll.
* Default value is 1.
*/
numScroll?: number | undefined;
/**
* An array of options for responsive design.
* @see CarouselResponsiveOptions
*/
responsiveOptions?: CarouselResponsiveOptions[] | undefined;
/**
* Specifies the layout of the component, valid values are 'horizontal' and 'vertical'.
* @see CarouselOrientationType
* Default value is 'horizontal'.
*/
orientation?: CarouselOrientationType;
/**
* Height of the viewport in vertical layout.
* Default value is '300px'.
*/
verticalViewPortHeight?: string | undefined;
/**
* Style class of the viewport container.
*/
containerClass?: any;
/**
* Style class of main content.
*/
contentClass?: any;
/**
* Style class of the indicator items.
*/
indicatorsContentClass?: any;
/**
* Defines if scrolling would be infinite.
*/
circular?: boolean | undefined;
/**
* Time in milliseconds to scroll items automatically.
* Default value is 0.
*/
autoplayInterval?: number | undefined;
}
export interface CarouselSlots {
/**
* Custom content for each item.
* @param {Object} scope - item slot's params.
*/
item: (scope: {
/**
* Data of the component
*/
data: any;
/**
* Index of the item
*/
index: number;
}) => VNode[];
/**
* Custom header template.
*/
header: () => VNode[];
/**
* Custom footer template.
*/
footer: () => VNode[];
}
export declare type CarouselEmits = {
/**
* Emitted when the page changes.
* @param {number} value - New page value.
*/
'update:page': (value: number) => void;
}
declare class Carousel extends ClassComponent<CarouselProps, CarouselSlots, CarouselEmits> { }
declare module '@vue/runtime-core' {
interface GlobalComponents {
Carousel: GlobalComponentConstructor<Carousel>
}
}
/**
*
* Carousel is a content slider featuring various customization options.
*
* Demos:
*
* - [Carousel](https://www.primefaces.org/primevue/showcase/#/carousel)
*
*/
export default Carousel;

View File

@ -0,0 +1,84 @@
import { mount } from '@vue/test-utils';
import Carousel from './Carousel.vue';
describe('Carousel.vue', () => {
it('should exist', async () => {
const wrapper = mount(Carousel, {
props: {
value: [
{
"id": "1000",
"code": "vbb124btr",
"name": "Game Controller",
"description": "Product Description",
"image": "game-controller.jpg",
"price": 99,
"category": "Electronics",
"quantity": 2,
"inventoryStatus": "LOWSTOCK",
"rating": 4
},
{
"id": "1001",
"code": "nvklal433",
"name": "Black Watch",
"description": "Product Description",
"image": "black-watch.jpg",
"price": 72,
"category": "Accessories",
"quantity": 61,
"inventoryStatus": "INSTOCK",
"rating": 4
},
{
"id": "1002",
"code": "zz21cz3c1",
"name": "Blue Band",
"description": "Product Description",
"image": "blue-band.jpg",
"price": 79,
"category": "Fitness",
"quantity": 2,
"inventoryStatus": "LOWSTOCK",
"rating": 3
},
{
"id": "1003",
"code": "244wgerg2",
"name": "Blue T-Shirt",
"description": "Product Description",
"image": "blue-t-shirt.jpg",
"price": 29,
"category": "Clothing",
"quantity": 25,
"inventoryStatus": "INSTOCK",
"rating": 5
}
]
},
slots: {
header: 'Basic',
item: `<template #item="slotProps">
<div class="product-item">
<div class="product-item-content">
<div class="mb-3">
<img :src="'demo/images/product/' + slotProps.data.image" :alt="slotProps.data.name" class="product-image" />
</div>
</div>
</div>
</template>
`
}
});
expect(wrapper.findAll('.p-carousel-item').length).toBe(4);
const firstItem = wrapper.findAll('.p-carousel-item')[0];
expect(firstItem.classes()).toContain('p-carousel-item-active');
const nextBtn = wrapper.find('.p-carousel-next');
await nextBtn.trigger('click');
expect(firstItem.classes()).not.toContain('p-carousel-item-active');
})
});

599
components/carousel/Carousel.vue Executable file
View File

@ -0,0 +1,599 @@
<template>
<div :id="id" :class="['p-carousel p-component', {'p-carousel-vertical': isVertical(), 'p-carousel-horizontal': !isVertical()}]">
<div class="p-carousel-header" v-if="$slots.header">
<slot name="header"></slot>
</div>
<div :class="contentClasses">
<div :class="containerClasses">
<button :class="['p-carousel-prev p-link', {'p-disabled': backwardIsDisabled}]" :disabled="backwardIsDisabled" @click="navBackward" type="button" v-ripple>
<span :class="['p-carousel-prev-icon pi', {'pi-chevron-left': !isVertical(),'pi-chevron-up': isVertical()}]"></span>
</button>
<div class="p-carousel-items-content" :style="[{'height': isVertical() ? verticalViewPortHeight : 'auto'}]" @touchend="onTouchEnd" @touchstart="onTouchStart" @touchmove="onTouchMove">
<div ref="itemsContainer" class="p-carousel-items-container" @transitionend="onTransitionEnd">
<template v-if="isCircular()">
<div v-for="(item, index) of value.slice(-1 * d_numVisible)" :key="index + '_scloned'" :class="['p-carousel-item p-carousel-item-cloned',
{'p-carousel-item-active': (totalShiftedItems * -1) === (value.length + d_numVisible),
'p-carousel-item-start': 0 === index,
'p-carousel-item-end': value.slice(-1 * d_numVisible).length - 1 === index}]">
<slot name="item" :data="item" :index="index"></slot>
</div>
</template>
<div v-for="(item, index) of value" :key="index" :class="['p-carousel-item',
{'p-carousel-item-active': firstIndex() <= index && lastIndex() >= index,
'p-carousel-item-start': firstIndex() === index,
'p-carousel-item-end': lastIndex() === index}]">
<slot name="item" :data="item" :index="index"></slot>
</div>
<template v-if="isCircular()">
<div v-for="(item, index) of value.slice(0, d_numVisible)" :key="index + '_fcloned'" :class="['p-carousel-item p-carousel-item-cloned',
{'p-carousel-item-active': totalShiftedItems === 0,
'p-carousel-item-start': 0 === index,
'p-carousel-item-end': value.slice(0, d_numVisible).length - 1 === index}]">
<slot name="item" :data="item" :index="index"></slot>
</div>
</template>
</div>
</div>
<button :class="['p-carousel-next p-link', {'p-disabled': forwardIsDisabled}]" :disabled="forwardIsDisabled" @click="navForward" type="button" v-ripple>
<span :class="['p-carousel-prev-icon pi', {'pi-chevron-right': !isVertical(),'pi-chevron-down': isVertical()}]"></span>
</button>
</div>
<ul v-if="totalIndicators >= 0" :class="indicatorsContentClasses">
<li v-for="(indicator, i) of totalIndicators" :key="'p-carousel-indicator-' + i.toString()" :class="['p-carousel-indicator', {'p-highlight': d_page === i}]">
<button class="p-link" @click="onIndicatorClick($event, i)" type="button" />
</li>
</ul>
</div>
<div class="p-carousel-footer" v-if="$slots.footer">
<slot name="footer"></slot>
</div>
</div>
</template>
<script>
import {UniqueComponentId} from 'primevue/utils';
import {DomHandler} from 'primevue/utils';
import Ripple from 'primevue/ripple';
export default {
name: 'Carousel',
emits: ['update:page'],
props: {
value: null,
page: {
type: Number,
default: 0
},
numVisible: {
type: Number,
default: 1
},
numScroll: {
type: Number,
default: 1
},
responsiveOptions: Array,
orientation: {
type: String,
default: 'horizontal'
},
verticalViewPortHeight: {
type: String,
default: '300px'
},
contentClass: String,
containerClass: String,
indicatorsContentClass: String,
circular: {
type: Boolean,
default: false
},
autoplayInterval: {
type: Number,
default:0
}
},
data() {
return {
id : UniqueComponentId(),
remainingItems: 0,
d_numVisible: this.numVisible,
d_numScroll: this.numScroll,
d_oldNumScroll: 0,
d_oldNumVisible: 0,
d_oldValue: null,
d_page: this.page,
totalShiftedItems: this.page * this.numScroll * -1,
allowAutoplay : !!this.autoplayInterval,
d_circular : this.circular || this.allowAutoplay,
swipeThreshold: 20
}
},
isRemainingItemsAdded: false,
watch: {
page(newValue) {
this.d_page = newValue;
},
circular(newValue) {
this.d_circular = newValue;
},
numVisible(newValue, oldValue) {
this.d_numVisible = newValue;
this.d_oldNumVisible = oldValue;
},
numScroll(newValue, oldValue) {
this.d_oldNumScroll = oldValue;
this.d_numScroll = newValue;
},
value(oldValue) {
this.d_oldValue =oldValue;
}
},
methods: {
step(dir, page) {
let totalShiftedItems = this.totalShiftedItems;
const isCircular = this.isCircular();
if (page != null) {
totalShiftedItems = (this.d_numScroll * page) * -1;
if (isCircular) {
totalShiftedItems -= this.d_numVisible;
}
this.isRemainingItemsAdded = false;
}
else {
totalShiftedItems += (this.d_numScroll * dir);
if (this.isRemainingItemsAdded) {
totalShiftedItems += this.remainingItems - (this.d_numScroll * dir);
this.isRemainingItemsAdded = false;
}
let originalShiftedItems = isCircular ? (totalShiftedItems + this.d_numVisible) : totalShiftedItems;
page = Math.abs(Math.floor(originalShiftedItems / this.d_numScroll));
}
if (isCircular && this.d_page === (this.totalIndicators - 1) && dir === -1) {
totalShiftedItems = -1 * (this.value.length + this.d_numVisible);
page = 0;
}
else if (isCircular && this.d_page === 0 && dir === 1) {
totalShiftedItems = 0;
page = (this.totalIndicators - 1);
}
else if (page === (this.totalIndicators - 1) && this.remainingItems > 0) {
totalShiftedItems += ((this.remainingItems * -1) - (this.d_numScroll * dir));
this.isRemainingItemsAdded = true;
}
if (this.$refs.itemsContainer) {
DomHandler.removeClass(this.$refs.itemsContainer, 'p-items-hidden');
this.$refs.itemsContainer.style.transform = this.isVertical() ? `translate3d(0, ${totalShiftedItems * (100/ this.d_numVisible)}%, 0)` : `translate3d(${totalShiftedItems * (100/ this.d_numVisible)}%, 0, 0)`;
this.$refs.itemsContainer.style.transition = 'transform 500ms ease 0s';
}
this.totalShiftedItems = totalShiftedItems;
this.$emit('update:page', page);
this.d_page = page;
},
calculatePosition() {
if (this.$refs.itemsContainer && this.responsiveOptions) {
let windowWidth = window.innerWidth;
let matchedResponsiveOptionsData = {
numVisible: this.numVisible,
numScroll: this.numScroll
};
for (let i = 0; i < this.responsiveOptions.length; i++) {
let res = this.responsiveOptions[i];
if (parseInt(res.breakpoint, 10) >= windowWidth) {
matchedResponsiveOptionsData = res;
}
}
if (this.d_numScroll !== matchedResponsiveOptionsData.numScroll) {
let page = this.d_page;
page = parseInt((page * this.d_numScroll) / matchedResponsiveOptionsData.numScroll);
this.totalShiftedItems = (matchedResponsiveOptionsData.numScroll * page) * -1;
if (this.isCircular()) {
this.totalShiftedItems -= matchedResponsiveOptionsData.numVisible;
}
this.d_numScroll = matchedResponsiveOptionsData.numScroll;
this.$emit('update:page', page);
this.d_page = page;
}
if (this.d_numVisible !== matchedResponsiveOptionsData.numVisible) {
this.d_numVisible = matchedResponsiveOptionsData.numVisible;
}
}
},
navBackward(e,index){
if (this.d_circular || this.d_page !== 0) {
this.step(1, index);
}
this.allowAutoplay = false;
if (e.cancelable) {
e.preventDefault();
}
},
navForward(e,index){
if (this.d_circular || this.d_page < (this.totalIndicators - 1)) {
this.step(-1, index);
}
this.allowAutoplay = false;
if (e.cancelable) {
e.preventDefault();
}
},
onIndicatorClick(e, index) {
let page = this.d_page;
if (index > page) {
this.navForward(e, index);
}
else if (index < page) {
this.navBackward(e, index);
}
},
onTransitionEnd() {
if (this.$refs.itemsContainer) {
DomHandler.addClass(this.$refs.itemsContainer, 'p-items-hidden');
this.$refs.itemsContainer.style.transition = '';
if ((this.d_page === 0 || this.d_page === (this.totalIndicators - 1)) && this.isCircular()) {
this.$refs.itemsContainer.style.transform = this.isVertical() ? `translate3d(0, ${this.totalShiftedItems * (100/ this.d_numVisible)}%, 0)` : `translate3d(${this.totalShiftedItems * (100/ this.d_numVisible)}%, 0, 0)`;
}
}
},
onTouchStart(e) {
let touchobj = e.changedTouches[0];
this.startPos = {
x: touchobj.pageX,
y: touchobj.pageY
};
},
onTouchMove(e) {
if (e.cancelable) {
e.preventDefault();
}
},
onTouchEnd(e) {
let touchobj = e.changedTouches[0];
if (this.isVertical()) {
this.changePageOnTouch(e, (touchobj.pageY - this.startPos.y));
}
else {
this.changePageOnTouch(e, (touchobj.pageX - this.startPos.x));
}
},
changePageOnTouch(e, diff) {
if (Math.abs(diff) > this.swipeThreshold) {
if (diff < 0) { // left
this.navForward(e);
}
else { // right
this.navBackward(e);
}
}
},
bindDocumentListeners() {
if (!this.documentResizeListener) {
this.documentResizeListener = (e) => {
this.calculatePosition(e);
};
window.addEventListener('resize', this.documentResizeListener);
}
},
unbindDocumentListeners() {
if(this.documentResizeListener) {
window.removeEventListener('resize', this.documentResizeListener);
this.documentResizeListener = null;
}
},
startAutoplay() {
this.interval = setInterval(() => {
if(this.d_page === (this.totalIndicators - 1)) {
this.step(-1, 0);
}
else {
this.step(-1, this.d_page + 1);
}
},
this.autoplayInterval);
},
stopAutoplay() {
if (this.interval) {
clearInterval(this.interval);
}
},
createStyle() {
if (!this.carouselStyle) {
this.carouselStyle = document.createElement('style');
this.carouselStyle.type = 'text/css';
document.body.appendChild(this.carouselStyle);
}
let innerHTML = `
#${this.id} .p-carousel-item {
flex: 1 0 ${ (100/ this.d_numVisible) }%
}
`;
if (this.responsiveOptions) {
let _responsiveOptions = [...this.responsiveOptions];
_responsiveOptions.sort((data1, data2) => {
const value1 = data1.breakpoint;
const value2 = data2.breakpoint;
let result = null;
if (value1 == null && value2 != null)
result = -1;
else if (value1 != null && value2 == null)
result = 1;
else if (value1 == null && value2 == null)
result = 0;
else if (typeof value1 === 'string' && typeof value2 === 'string')
result = value1.localeCompare(value2, undefined, { numeric: true });
else
result = (value1 < value2) ? -1 : (value1 > value2) ? 1 : 0;
return -1 * result;
});
for (let i = 0; i < _responsiveOptions.length; i++) {
let res = _responsiveOptions[i];
innerHTML += `
@media screen and (max-width: ${res.breakpoint}) {
#${this.id} .p-carousel-item {
flex: 1 0 ${ (100/ res.numVisible) }%
}
}
`
}
}
this.carouselStyle.innerHTML = innerHTML;
},
isVertical() {
return this.orientation === 'vertical';
},
isCircular() {
return this.value && this.d_circular && this.value.length >= this.d_numVisible;
},
isAutoplay() {
return this.autoplayInterval && this.allowAutoplay;
},
firstIndex() {
return this.isCircular()? (-1 * (this.totalShiftedItems + this.d_numVisible)) : (this.totalShiftedItems * -1);
},
lastIndex() {
return (this.firstIndex() + this.d_numVisible - 1);
}
},
mounted() {
let stateChanged = false;
this.createStyle();
this.calculatePosition();
if (this.responsiveOptions) {
this.bindDocumentListeners();
}
if (this.isCircular()) {
let totalShiftedItems = this.totalShiftedItems;
if (this.d_page === 0) {
totalShiftedItems = -1 * this.d_numVisible;
}
else if (totalShiftedItems === 0) {
totalShiftedItems = -1 * this.value.length;
if (this.remainingItems > 0) {
this.isRemainingItemsAdded = true;
}
}
if (totalShiftedItems !== this.totalShiftedItems) {
this.totalShiftedItems = totalShiftedItems;
stateChanged = true;
}
}
if (!stateChanged && this.isAutoplay()) {
this.startAutoplay();
}
},
updated() {
const isCircular = this.isCircular();
let stateChanged = false;
let totalShiftedItems = this.totalShiftedItems;
if (this.autoplayInterval) {
this.stopAutoplay();
}
if(this.d_oldNumScroll !== this.d_numScroll || this.d_oldNumVisible !== this.d_numVisible || this.d_oldValue.length !== this.value.length) {
this.remainingItems = (this.value.length - this.d_numVisible) % this.d_numScroll;
let page = this.d_page;
if (this.totalIndicators !== 0 && page >= this.totalIndicators) {
page = this.totalIndicators - 1;
this.$emit('update:page', page);
this.d_page = page;
stateChanged = true;
}
totalShiftedItems = (page * this.d_numScroll) * -1;
if (isCircular) {
totalShiftedItems -= this.d_numVisible;
}
if (page === (this.totalIndicators - 1) && this.remainingItems > 0) {
totalShiftedItems += (-1 * this.remainingItems) + this.d_numScroll;
this.isRemainingItemsAdded = true;
}
else {
this.isRemainingItemsAdded = false;
}
if (totalShiftedItems !== this.totalShiftedItems) {
this.totalShiftedItems = totalShiftedItems;
stateChanged = true;
}
this.d_oldNumScroll = this.d_numScroll;
this.d_oldNumVisible = this.d_numVisible;
this.d_oldValue = this.value;
this.$refs.itemsContainer.style.transform = this.isVertical() ? `translate3d(0, ${totalShiftedItems * (100/ this.d_numVisible)}%, 0)` : `translate3d(${totalShiftedItems * (100/ this.d_numVisible)}%, 0, 0)`;
}
if (isCircular) {
if (this.d_page === 0) {
totalShiftedItems = -1 * this.d_numVisible;
}
else if (totalShiftedItems === 0) {
totalShiftedItems = -1 * this.value.length;
if (this.remainingItems > 0) {
this.isRemainingItemsAdded = true;
}
}
if (totalShiftedItems !== this.totalShiftedItems) {
this.totalShiftedItems = totalShiftedItems;
stateChanged = true;
}
}
if (!stateChanged && this.isAutoplay()) {
this.startAutoplay();
}
},
beforeUnmount() {
if (this.responsiveOptions) {
this.unbindDocumentListeners();
}
if (this.autoplayInterval) {
this.stopAutoplay();
}
},
computed: {
totalIndicators() {
return this.value ? Math.max(Math.ceil((this.value.length - this.d_numVisible) / this.d_numScroll) + 1, 0) : 0;
},
backwardIsDisabled() {
return (this.value && (!this.circular || this.value.length < this.d_numVisible) && this.d_page === 0);
},
forwardIsDisabled() {
return (this.value && (!this.circular || this.value.length < this.d_numVisible) && (this.d_page === (this.totalIndicators - 1) || this.totalIndicators === 0));
},
containerClasses() {
return ['p-carousel-container', this.containerClass];
},
contentClasses() {
return ['p-carousel-content ', this.contentClass];
},
indicatorsContentClasses() {
return ['p-carousel-indicators p-reset', this.indicatorsContentClass];
},
},
directives: {
'ripple': Ripple
}
}
</script>
<style>
.p-carousel {
display: flex;
flex-direction: column;
}
.p-carousel-content {
display: flex;
flex-direction: column;
overflow: auto;
}
.p-carousel-prev,
.p-carousel-next {
align-self: center;
flex-grow: 0;
flex-shrink: 0;
display: flex;
justify-content: center;
align-items: center;
overflow: hidden;
position: relative;
}
.p-carousel-container {
display: flex;
flex-direction: row;
}
.p-carousel-items-content {
overflow: hidden;
width: 100%;
}
.p-carousel-items-container {
display: flex;
flex-direction: row;
}
.p-carousel-indicators {
display: flex;
flex-direction: row;
justify-content: center;
flex-wrap: wrap;
}
.p-carousel-indicator > button {
display: flex;
align-items: center;
justify-content: center;
}
/* Vertical */
.p-carousel-vertical .p-carousel-container {
flex-direction: column;
}
.p-carousel-vertical .p-carousel-items-container {
flex-direction: column;
height: 100%;
}
/* Keyboard Support */
.p-items-hidden .p-carousel-item {
visibility: hidden;
}
.p-items-hidden .p-carousel-item.p-carousel-item-active {
visibility: visible;
}
</style>

View File

@ -0,0 +1,9 @@
{
"main": "./carousel.cjs.js",
"module": "./carousel.esm.js",
"unpkg": "./carousel.min.js",
"types": "./Carousel.d.ts",
"browser": {
"./sfc": "./Carousel.vue"
}
}

View File

@ -0,0 +1,264 @@
import { HTMLAttributes, InputHTMLAttributes, VNode } from 'vue';
import { ClassComponent, GlobalComponentConstructor } from '../ts-helpers';
type CascadeSelectOptionLabelType = string | ((data: any) => string) | undefined;
type CascadeSelectOptionValueType = string | ((data: any) => any) | undefined;
type CascadeSelectOptionDisabledType = string | ((data: any) => boolean) | undefined;
type CascadeSelectOptionChildrenType = string[] | string | ((data: any) => any[]) | undefined;
type CascadeSelectAppendToType = 'body' | 'self' | string | undefined | HTMLElement;
export interface CascadeSelectChangeEvent {
/**
* Original event.
*/
originalEvent: Event;
/**
* Selected option value.
*/
value: any;
}
/**
* @extends CascadeSelectChangeEvent
*/
export interface CascadeSelectGroupChangeEvent extends CascadeSelectChangeEvent { }
export interface CascadeSelectProps {
/**
* Value of the component.
*/
modelValue?: any | undefined;
/**
* An array of selectitems to display as the available options.
*/
options?: any[] | undefined;
/**
* Property name or getter function to use as the label of an option.
* @see CascadeSelectOptionLabelType
*/
optionLabel?: CascadeSelectOptionLabelType;
/**
* Property name or getter function to use as the value of an option, defaults to the option itself when not defined.
* @see CascadeSelectOptionValueType
*/
optionValue?: CascadeSelectOptionValueType;
/**
* Property name or getter function to use as the disabled flag of an option, defaults to false when not defined.
* @see CascadeSelectOptionDisabledType
*/
optionDisabled?: CascadeSelectOptionDisabledType;
/**
* Property name or getter function to use as the label of an option group.
* @see CascadeSelectOptionLabelType
*/
optionGroupLabel?: CascadeSelectOptionLabelType;
/**
* Property name or getter function to retrieve the items of a group.
* @see CascadeSelectOptionChildrenType
*/
optionGroupChildren?: CascadeSelectOptionChildrenType;
/**
* Default text to display when no option is selected.
*/
placeholder?: string | undefined;
/**
* When present, it specifies that the component should be disabled.
*/
disabled?: boolean | undefined;
/**
* A property to uniquely identify an option.
*/
dataKey?: string | undefined;
/**
* Identifier of the underlying input element.
*/
inputId?: string | undefined;
/**
* Inline style of the input field.
*/
inputStyle?: any;
/**
* Style class of the input field.
*/
inputClass?: any;
/**
* Uses to pass all properties of the HTMLInputElement to the focusable input element inside the component.
*/
inputProps?: InputHTMLAttributes | undefined;
/**
* Inline style of the overlay panel.
*/
panelStyle?: any;
/**
* Style class of the overlay panel.
*/
panelClass?: any;
/**
* Uses to pass all properties of the HTMLDivElement to the overlay panel inside the component.
*/
panelProps?: HTMLAttributes | undefined;
/**
* A valid query selector or an HTMLElement to specify where the overlay gets attached. Special keywords are 'body' for document body and 'self' for the element itself.
* @see CascadeSelectAppendToType
* Default value is 'body'.
*/
appendTo?: CascadeSelectAppendToType;
/**
* Whether the dropdown is in loading state.
*/
loading?: boolean | undefined;
/**
* Icon to display in loading state.
* Default value is 'pi pi-spinner pi-spin'.
*/
loadingIcon?: string | undefined;
/**
* Whether to focus on the first visible or selected element when the overlay panel is shown.
* Default value is true.
*/
autoOptionFocus?: boolean | undefined;
/**
* Locale to use in searching. The default locale is the host environment's current locale.
*/
searchLocale?: string | undefined;
/**
* Text to be displayed in hidden accessible field when filtering returns any results. Defaults to value from PrimeVue locale configuration.
* Default value is '{0} results are available'.
*/
searchMessage?: string | undefined;
/**
* Text to be displayed in hidden accessible field when options are selected. Defaults to value from PrimeVue locale configuration.
* Default value is '{0} items selected'.
*/
selectionMessage?: string | undefined;
/**
* Text to be displayed in hidden accessible field when any option is not selected. Defaults to value from PrimeVue locale configuration.
* Default value is 'No selected item'.
*/
emptySelectionMessage?: string | undefined;
/**
* Text to display when filtering does not return any results. Defaults to value from PrimeVue locale configuration.
* Default value is 'No results found'.
*/
emptySearchMessage?: string | undefined;
/**
* Text to be displayed when there are no options available. Defaults to value from PrimeVue locale configuration.
* Default value is 'No available options'.
*/
emptyMessage?: string | undefined;
/**
* Index of the element in tabbing order.
*/
tabindex?: number | string | undefined;
/**
* Establishes relationships between the component and label(s) where its value should be one or more element IDs.
*/
'aria-labelledby'?: string | undefined;
/**
* Establishes a string value that labels the component.
*/
'aria-label'?: string | undefined;
}
export interface CascadeSelectSlots {
/**
* Custom content for each option.
* @param {Object} scope - option slot's params.
*/
option: (scope: {
/**
* Option instance
*/
option: any;
}) => VNode[];
/**
* Custom value template.
* @param {CascadeSelectValueSlot} scope - value slot's params.
*/
value: (scope: {
/**
* Value of the component
*/
value: any;
/**
* Placeholder text to show
*/
placeholder: string;
}) => VNode[];
/**
* Custom indicator template.
*/
indicator: () => VNode[];
}
export declare type CascadeSelectEmits = {
/**
* Emitted when the value changes.
* @param {*} value - New value.
*/
'update:modelValue': (value: any) => void;
/**
* Callback to invoke on value change.
* @param { CascadeSelectChangeEvent } event - Custom change event.
*/
'change': (event: CascadeSelectChangeEvent) => void;
/**
* Callback to invoke when the component receives focus.
* @param {Event} event - Browser event.
*/
'focus': (event: Event) => void;
/**
* Callback to invoke when the component loses focus.
* @param {Event} event - Browser event.
*/
'blur': (event: Event) => void;
/**
* Callback to invoke on click.
* @param { Event } event - Browser event.
*/
'click': (event: Event) => void;
/**
* Callback to invoke when a group changes.
* @param { CascadeSelectGroupChangeEvent } event - Custom change event.
*/
'group-change': (event: CascadeSelectGroupChangeEvent) => void;
/**
* Callback to invoke before the overlay is shown.
*/
'before-show': () => void;
/**
* Callback to invoke before the overlay is hidden.
*/
'before-hide': () => void;
/**
* Callback to invoke when the overlay is shown.
*/
'show': () => void;
/**
* Callback to invoke when the overlay is hidden.
*/
'hide': () => void;
}
declare class CascadeSelect extends ClassComponent<CascadeSelectProps, CascadeSelectSlots, CascadeSelectEmits> { }
declare module '@vue/runtime-core' {
interface GlobalComponents {
CascadeSelect: GlobalComponentConstructor<CascadeSelect>
}
}
/**
*
* CascadeSelect displays a nested structure of options.
*
* Demos:
*
* - [CascadeSelect](https://www.primefaces.org/primevue/showcase/#/cascadeselect)
*
*/
export default CascadeSelect;

View File

@ -0,0 +1,124 @@
import { mount } from '@vue/test-utils';
import PrimeVue from '@/components/config/PrimeVue';
import CascadeSelect from './CascadeSelect.vue';
describe('CascadeSelect.vue', () => {
let wrapper;
beforeEach(() => {
wrapper = mount(CascadeSelect, {
global: {
plugins: [PrimeVue],
stubs: {
teleport: true
}
},
props: {
modelValue: null,
options: [
{
name: 'Australia',
code: 'AU',
states: [
{
name: 'New South Wales',
cities: [
{cname: 'Sydney', code: 'A-SY'},
{cname: 'Newcastle', code: 'A-NE'},
{cname: 'Wollongong', code: 'A-WO'}
]
},
{
name: 'Queensland',
cities: [
{cname: 'Brisbane', code: 'A-BR'},
{cname: 'Townsville', code: 'A-TO'}
]
},
]
},
{
name: 'Canada',
code: 'CA',
states: [
{
name: 'Quebec',
cities: [
{cname: 'Montreal', code: 'C-MO'},
{cname: 'Quebec City', code: 'C-QU'}
]
},
{
name: 'Ontario',
cities: [
{cname: 'Ottawa', code: 'C-OT'},
{cname: 'Toronto', code: 'C-TO'}
]
},
]
},
{
name: 'United States',
code: 'US',
states: [
{
name: 'California',
cities: [
{cname: 'Los Angeles', code: 'US-LA'},
{cname: 'San Diego', code: 'US-SD'},
{cname: 'San Francisco', code: 'US-SF'}
]
},
{
name: 'Florida',
cities: [
{cname: 'Jacksonville', code: 'US-JA'},
{cname: 'Miami', code: 'US-MI'},
{cname: 'Tampa', code: 'US-TA'},
{cname: 'Orlando', code: 'US-OR'}
]
},
{
name: 'Texas',
cities: [
{cname: 'Austin', code: 'US-AU'},
{cname: 'Dallas', code: 'US-DA'},
{cname: 'Houston', code: 'US-HO'}
]
}
]
}
],
optionLabel: 'cname',
optionGroupLabel: 'name',
optionGroupChildren: ['states', 'cities']
}
});
});
it('should exist', async() => {
expect(wrapper.find('.p-cascadeselect.p-component').exists()).toBe(true);
});
it('should show list and sublist', async() => {
expect(wrapper.find('.p-cascadeselect.p-component').exists()).toBe(true);
await wrapper.trigger('click');
expect(wrapper.find('.p-cascadeselect-panel.p-cascadeselect-items').exists()).toBe(true);
expect(wrapper.findAll('.p-cascadeselect-item').length).toBe(3);
expect(wrapper.findAll('.p-cascadeselect-item-text')[0].text()).toBe('Australia');
const firstGroup = wrapper.findAll('.p-cascadeselect-item-content')[0];
await firstGroup.trigger('click');
expect(wrapper.find('.p-cascadeselect-panel.p-cascadeselect-sublist').exists()).toBe(true);
const sublist = wrapper.find('.p-cascadeselect-panel.p-cascadeselect-sublist');
expect(sublist.findAll('.p-cascadeselect-item.p-cascadeselect-item-group').length).toBe(2);
expect(sublist.findAll('.p-cascadeselect-item-text')[0].text()).toBe('New South Wales');
});
});

View File

@ -0,0 +1,851 @@
<template>
<div ref="container" :class="containerClass" @click="onContainerClick($event)">
<div class="p-hidden-accessible">
<input ref="focusInput" :id="inputId" type="text" :style="inputStyle" :class="inputClass" readonly :disabled="disabled" :placeholder="placeholder" :tabindex="!disabled ? tabindex : -1"
role="combobox" :aria-label="ariaLabel" :aria-labelledby="ariaLabelledby" aria-haspopup="tree" :aria-expanded="overlayVisible" :aria-controls="id + '_tree'" :aria-activedescendant="focused ? focusedOptionId : undefined"
@focus="onFocus" @blur="onBlur" @keydown="onKeyDown" v-bind="inputProps" />
</div>
<span :class="labelClass">
<slot name="value" :value="modelValue" :placeholder="placeholder">
{{label}}
</slot>
</span>
<div class="p-cascadeselect-trigger" role="button" tabindex="-1" aria-hidden="true">
<slot name="indicator">
<span :class="dropdownIconClass"></span>
</slot>
</div>
<span role="status" aria-live="polite" class="p-hidden-accessible">
{{searchResultMessageText}}
</span>
<Portal :appendTo="appendTo">
<transition name="p-connected-overlay" @enter="onOverlayEnter" @after-enter="onOverlayAfterEnter" @leave="onOverlayLeave" @after-leave="onOverlayAfterLeave">
<div v-if="overlayVisible" :ref="overlayRef" :style="panelStyle" :class="panelStyleClass" @click="onOverlayClick" @keydown="onOverlayKeyDown" v-bind="panelProps">
<div class="p-cascadeselect-items-wrapper">
<CascadeSelectSub :id="id + '_tree'" role="tree" aria-orientation="horizontal" :selectId="id" :focusedOptionId="focused ? focusedOptionId : undefined"
:options="processedOptions" :activeOptionPath="activeOptionPath" :level="0" :templates="$slots" :optionLabel="optionLabel" :optionValue="optionValue" :optionDisabled="optionDisabled"
:optionGroupLabel="optionGroupLabel" :optionGroupChildren="optionGroupChildren" @option-change="onOptionChange" />
<span role="status" aria-live="polite" class="p-hidden-accessible">
{{selectedMessageText}}
</span>
</div>
</div>
</transition>
</Portal>
</div>
</template>
<script>
import {ConnectedOverlayScrollHandler,ObjectUtils,DomHandler,ZIndexUtils,UniqueComponentId} from 'primevue/utils';
import OverlayEventBus from 'primevue/overlayeventbus';
import CascadeSelectSub from './CascadeSelectSub.vue';
import Portal from 'primevue/portal';
export default {
name: 'CascadeSelect',
emits: ['update:modelValue', 'change', 'focus', 'blur', 'click', 'group-change', 'before-show', 'before-hide', 'hide', 'show'],
props: {
modelValue: null,
options: Array,
optionLabel: null,
optionValue: null,
optionDisabled: null,
optionGroupLabel: null,
optionGroupChildren: null,
placeholder: String,
disabled: Boolean,
dataKey: null,
inputId: null,
inputStyle: null,
inputClass: null,
inputProps: null,
panelStyle: null,
panelClass: null,
panelProps: null,
appendTo: {
type: String,
default: 'body'
},
loading: {
type: Boolean,
default: false
},
loadingIcon: {
type: String,
default: 'pi pi-spinner pi-spin'
},
autoOptionFocus: {
type: Boolean,
default: true
},
searchLocale: {
type: String,
default: undefined
},
searchMessage: {
type: String,
default: null
},
selectionMessage: {
type: String,
default: null
},
emptySelectionMessage: {
type: String,
default: null
},
emptySearchMessage: {
type: String,
default: null
},
emptyMessage: {
type: String,
default: null
},
tabindex: {
type: Number,
default: 0
},
'aria-labelledby': {
type: String,
default: null
},
'aria-label': {
type: String,
default: null
}
},
outsideClickListener: null,
scrollHandler: null,
resizeListener: null,
overlay: null,
searchTimeout: null,
searchValue: null,
selectOnFocus: false,
focusOnHover: false,
data() {
return {
id: UniqueComponentId(),
focused: false,
focusedOptionInfo: { index: -1, level: 0, parentKey: '' },
activeOptionPath: [],
overlayVisible: false,
dirty: false
}
},
watch: {
options() {
this.autoUpdateModel();
}
},
mounted() {
this.id = this.$attrs.id || this.id;
},
beforeUnmount() {
this.unbindOutsideClickListener();
this.unbindResizeListener();
if (this.scrollHandler) {
this.scrollHandler.destroy();
this.scrollHandler = null;
}
if (this.overlay) {
ZIndexUtils.clear(this.overlay);
this.overlay = null;
}
},
methods: {
getOptionLabel(option) {
return this.optionLabel ? ObjectUtils.resolveFieldData(option, this.optionLabel) : option;
},
getOptionValue(option) {
return this.optionValue ? ObjectUtils.resolveFieldData(option, this.optionValue) : option;
},
isOptionDisabled(option) {
return this.optionDisabled ? ObjectUtils.resolveFieldData(option, this.optionDisabled) : false;
},
getOptionGroupLabel(optionGroup) {
return this.optionGroupLabel ? ObjectUtils.resolveFieldData(optionGroup, this.optionGroupLabel) : null;
},
getOptionGroupChildren(optionGroup, level) {
return ObjectUtils.resolveFieldData(optionGroup, this.optionGroupChildren[level]);
},
isOptionGroup(option, level) {
return Object.prototype.hasOwnProperty.call(option, this.optionGroupChildren[level]);
},
getProccessedOptionLabel(processedOption) {
const grouped = this.isProccessedOptionGroup(processedOption);
return grouped ? this.getOptionGroupLabel(processedOption.option, processedOption.level) : this.getOptionLabel(processedOption.option);
},
isProccessedOptionGroup(processedOption) {
return ObjectUtils.isNotEmpty(processedOption.children);
},
show(isFocus) {
this.$emit('before-show');
this.overlayVisible = true;
this.activeOptionPath = this.findOptionPathByValue(this.modelValue);
if (this.hasSelectedOption && ObjectUtils.isNotEmpty(this.activeOptionPath)) {
const processedOption = this.activeOptionPath[this.activeOptionPath.length - 1];
this.focusedOptionInfo = { index: (this.autoOptionFocus ? processedOption.index : -1), level: processedOption.level, parentKey: processedOption.parentKey };
}
else {
this.focusedOptionInfo = { index: (this.autoOptionFocus ? this.findFirstFocusedOptionIndex() : -1), level: 0, parentKey: '' };
}
isFocus && this.$refs.focusInput.focus();
},
hide(isFocus) {
const _hide = () => {
this.$emit('before-hide');
this.overlayVisible = false;
this.activeOptionPath = [];
this.focusedOptionInfo = { index: -1, level: 0, parentKey: '' };
isFocus && this.$refs.focusInput.focus();
}
setTimeout(() => { _hide() }, 0); // For ScreenReaders
},
onFocus(event) {
this.focused = true;
this.$emit('focus', event);
},
onBlur(event) {
this.focused = false;
this.focusedOptionInfo = { index: -1, level: 0, parentKey: '' };
this.searchValue = '';
this.$emit('blur', event);
},
onKeyDown(event) {
if (this.disabled || this.loading) {
event.preventDefault();
return;
}
switch (event.code) {
case 'ArrowDown':
this.onArrowDownKey(event);
break;
case 'ArrowUp':
this.onArrowUpKey(event);
break;
case 'ArrowLeft':
this.onArrowLeftKey(event);
break;
case 'ArrowRight':
this.onArrowRightKey(event);
break;
case 'Home':
this.onHomeKey(event);
break;
case 'End':
this.onEndKey(event);
break;
case 'Space':
this.onSpaceKey(event);
break;
case 'Enter':
this.onEnterKey(event);
break;
case 'Escape':
this.onEscapeKey(event);
break;
case 'Tab':
this.onTabKey(event);
break;
case 'PageDown':
case 'PageUp':
case 'Backspace':
case 'ShiftLeft':
case 'ShiftRight':
//NOOP
break;
default:
if (ObjectUtils.isPrintableCharacter(event.key)) {
!this.overlayVisible && this.show();
this.searchOptions(event, event.key);
}
break;
}
},
onOptionChange(event) {
const { originalEvent, processedOption, isFocus } = event;
const { index, level, parentKey, children } = processedOption;
const grouped = ObjectUtils.isNotEmpty(children);
const activeOptionPath = this.activeOptionPath.filter(p => p.parentKey !== parentKey);
activeOptionPath.push(processedOption);
this.focusedOptionInfo = { index, level, parentKey };
this.activeOptionPath = activeOptionPath;
grouped ? this.onOptionGroupSelect(originalEvent, processedOption) : this.onOptionSelect(originalEvent, processedOption);
isFocus && this.$refs.focusInput.focus();
},
onOptionSelect(event, processedOption) {
const value = this.getOptionValue(processedOption.option);
this.activeOptionPath.forEach(p => p.selected = true);
this.updateModel(event, value);
this.hide(true);
},
onOptionGroupSelect(event, processedOption) {
this.dirty = true;
this.$emit('group-change', { originalEvent: event, value: processedOption.option });
},
onContainerClick(event) {
if (this.disabled || this.loading) {
return;
}
if (!this.overlay || !this.overlay.contains(event.target)) {
this.overlayVisible ? this.hide() : this.show();
this.$refs.focusInput.focus();
}
this.$emit('click', event);
},
onOverlayClick(event) {
OverlayEventBus.emit('overlay-click', {
originalEvent: event,
target: this.$el
});
},
onOverlayKeyDown(event) {
switch (event.code) {
case 'Escape':
this.onEscapeKey(event);
break;
default:
break;
}
},
onArrowDownKey(event) {
const optionIndex = this.focusedOptionInfo.index !== -1 ? this.findNextOptionIndex(this.focusedOptionInfo.index) : this.findFirstFocusedOptionIndex();
this.changeFocusedOptionIndex(event, optionIndex);
!this.overlayVisible && this.show();
event.preventDefault();
},
onArrowUpKey(event) {
if (event.altKey) {
if (this.focusedOptionInfo.index !== -1) {
const processedOption = this.visibleOptions[this.focusedOptionInfo.index];
const grouped = this.isProccessedOptionGroup(processedOption);
!grouped && this.onOptionChange({ originalEvent: event, processedOption });
}
this.overlayVisible && this.hide();
event.preventDefault();
}
else {
const optionIndex = this.focusedOptionInfo.index !== -1 ? this.findPrevOptionIndex(this.focusedOptionInfo.index) : this.findLastFocusedOptionIndex();
this.changeFocusedOptionIndex(event, optionIndex);
!this.overlayVisible && this.show();
event.preventDefault();
}
},
onArrowLeftKey(event) {
if (this.overlayVisible) {
const processedOption = this.visibleOptions[this.focusedOptionInfo.index];
const parentOption = this.activeOptionPath.find(p => p.key === processedOption.parentKey);
const matched = this.focusedOptionInfo.parentKey === '' || (parentOption && parentOption.key === this.focusedOptionInfo.parentKey);
const root = ObjectUtils.isEmpty(processedOption.parent);
if (matched) {
this.activeOptionPath = this.activeOptionPath.filter(p => p.parentKey !== this.focusedOptionInfo.parentKey);
}
if (!root) {
this.focusedOptionInfo = { index: -1, parentKey: parentOption ? parentOption.parentKey : '' };
this.searchValue = '';
this.onArrowDownKey(event);
}
event.preventDefault();
}
},
onArrowRightKey(event) {
if (this.overlayVisible) {
const processedOption = this.visibleOptions[this.focusedOptionInfo.index];
const grouped = this.isProccessedOptionGroup(processedOption);
if (grouped) {
const matched = this.activeOptionPath.some(p => processedOption.key === p.key);
if (matched) {
this.focusedOptionInfo = { index: -1, parentKey: processedOption.key };
this.searchValue = '';
this.onArrowDownKey(event);
}
else {
this.onOptionChange({ originalEvent: event, processedOption });
}
}
event.preventDefault();
}
},
onHomeKey(event) {
this.changeFocusedOptionIndex(event, this.findFirstOptionIndex());
!this.overlayVisible && this.show();
event.preventDefault();
},
onEndKey(event) {
this.changeFocusedOptionIndex(event, this.findLastOptionIndex());
!this.overlayVisible && this.show();
event.preventDefault();
},
onEnterKey(event) {
if (!this.overlayVisible) {
this.onArrowDownKey(event);
}
else {
if (this.focusedOptionInfo.index !== -1) {
const processedOption = this.visibleOptions[this.focusedOptionInfo.index];
const grouped = this.isProccessedOptionGroup(processedOption);
this.onOptionChange({ originalEvent: event, processedOption });
!grouped && this.hide();
}
}
event.preventDefault();
},
onSpaceKey(event) {
this.onEnterKey(event);
},
onEscapeKey(event) {
this.overlayVisible && this.hide(true);
event.preventDefault();
},
onTabKey(event) {
if (this.focusedOptionInfo.index !== -1) {
const processedOption = this.visibleOptions[this.focusedOptionInfo.index];
const grouped = this.isProccessedOptionGroup(processedOption);
!grouped && this.onOptionChange({ originalEvent: event, processedOption });
}
this.overlayVisible && this.hide();
},
onOverlayEnter(el) {
ZIndexUtils.set('overlay', el, this.$primevue.config.zIndex.overlay);
this.alignOverlay();
this.scrollInView();
},
onOverlayAfterEnter() {
this.bindOutsideClickListener();
this.bindScrollListener();
this.bindResizeListener();
this.$emit('show');
},
onOverlayLeave() {
this.unbindOutsideClickListener();
this.unbindScrollListener();
this.unbindResizeListener();
this.$emit('hide');
this.overlay = null;
this.dirty = false;
},
onOverlayAfterLeave(el) {
ZIndexUtils.clear(el);
},
alignOverlay() {
if (this.appendTo === 'self') {
DomHandler.relativePosition(this.overlay, this.$el);
}
else {
this.overlay.style.minWidth = DomHandler.getOuterWidth(this.$el) + 'px';
DomHandler.absolutePosition(this.overlay, this.$el);
}
},
bindOutsideClickListener() {
if (!this.outsideClickListener) {
this.outsideClickListener = (event) => {
if (this.overlayVisible && this.overlay && !this.$el.contains(event.target) && !this.overlay.contains(event.target)) {
this.hide();
}
};
document.addEventListener('click', this.outsideClickListener);
}
},
unbindOutsideClickListener() {
if (this.outsideClickListener) {
document.removeEventListener('click', this.outsideClickListener);
this.outsideClickListener = null;
}
},
bindScrollListener() {
if (!this.scrollHandler) {
this.scrollHandler = new ConnectedOverlayScrollHandler(this.$refs.container, () => {
if (this.overlayVisible) {
this.hide();
}
});
}
this.scrollHandler.bindScrollListener();
},
unbindScrollListener() {
if (this.scrollHandler) {
this.scrollHandler.unbindScrollListener();
}
},
bindResizeListener() {
if (!this.resizeListener) {
this.resizeListener = () => {
if (this.overlayVisible && !DomHandler.isTouchDevice()) {
this.hide();
}
};
window.addEventListener('resize', this.resizeListener);
}
},
unbindResizeListener() {
if (this.resizeListener) {
window.removeEventListener('resize', this.resizeListener);
this.resizeListener = null;
}
},
isOptionMatched(processedOption) {
return this.isValidOption(processedOption) && this.getProccessedOptionLabel(processedOption).toLocaleLowerCase(this.searchLocale).startsWith(this.searchValue.toLocaleLowerCase(this.searchLocale));
},
isValidOption(processedOption) {
return !!processedOption && !this.isOptionDisabled(processedOption.option);
},
isValidSelectedOption(processedOption) {
return this.isValidOption(processedOption) && this.isSelected(processedOption);
},
isSelected(processedOption) {
return this.activeOptionPath.some(p => p.key === processedOption.key);
},
findFirstOptionIndex() {
return this.visibleOptions.findIndex(processedOption => this.isValidOption(processedOption));
},
findLastOptionIndex() {
return ObjectUtils.findLastIndex(this.visibleOptions, processedOption => this.isValidOption(processedOption));
},
findNextOptionIndex(index) {
const matchedOptionIndex = index < (this.visibleOptions.length - 1) ? this.visibleOptions.slice(index + 1).findIndex(processedOption => this.isValidOption(processedOption)) : -1;
return matchedOptionIndex > -1 ? matchedOptionIndex + index + 1 : index;
},
findPrevOptionIndex(index) {
const matchedOptionIndex = index > 0 ? ObjectUtils.findLastIndex(this.visibleOptions.slice(0, index), processedOption => this.isValidOption(processedOption)) : -1;
return matchedOptionIndex > -1 ? matchedOptionIndex : index;
},
findSelectedOptionIndex() {
return this.visibleOptions.findIndex(processedOption => this.isValidSelectedOption(processedOption));
},
findFirstFocusedOptionIndex() {
const selectedIndex = this.findSelectedOptionIndex();
return selectedIndex < 0 ? this.findFirstOptionIndex() : selectedIndex;
},
findLastFocusedOptionIndex() {
const selectedIndex = this.findSelectedOptionIndex();
return selectedIndex < 0 ? this.findLastOptionIndex() : selectedIndex;
},
findOptionPathByValue(value, processedOptions, level = 0) {
processedOptions = processedOptions || (level === 0 && this.processedOptions);
if (!processedOptions) return null;
if (ObjectUtils.isEmpty(value)) return [];
for (let i = 0; i < processedOptions.length; i++) {
const processedOption = processedOptions[i];
if (ObjectUtils.equals(value, this.getOptionValue(processedOption.option), this.equalityKey)) {
return [processedOption];
}
const matchedOptions = this.findOptionPathByValue(value, processedOption.children, level + 1);
if (matchedOptions) {
matchedOptions.unshift(processedOption);
return matchedOptions;
}
}
},
searchOptions(event, char) {
this.searchValue = (this.searchValue || '') + char;
let optionIndex = -1;
let matched = false;
if (this.focusedOptionInfo.index !== -1) {
optionIndex = this.visibleOptions.slice(this.focusedOptionInfo.index).findIndex(processedOption => this.isOptionMatched(processedOption));
optionIndex = optionIndex === -1 ? this.visibleOptions.slice(0, this.focusedOptionInfo.index).findIndex(processedOption => this.isOptionMatched(processedOption)) : optionIndex + this.focusedOptionInfo.index;
}
else {
optionIndex = this.visibleOptions.findIndex(processedOption => this.isOptionMatched(processedOption));
}
if (optionIndex !== -1) {
matched = true;
}
if (optionIndex === -1 && this.focusedOptionInfo.index === -1) {
optionIndex = this.findFirstFocusedOptionIndex();
}
if (optionIndex !== -1) {
this.changeFocusedOptionIndex(event, optionIndex);
}
if (this.searchTimeout) {
clearTimeout(this.searchTimeout);
}
this.searchTimeout = setTimeout(() => {
this.searchValue = '';
this.searchTimeout = null;
}, 500);
return matched;
},
changeFocusedOptionIndex(event, index) {
if (this.focusedOptionInfo.index !== index) {
this.focusedOptionInfo.index = index;
this.scrollInView();
if (this.selectOnFocus) {
this.updateModel(event, this.getOptionValue(this.visibleOptions[index]));
}
}
},
scrollInView(index = -1) {
const id = index !== -1 ? `${this.id}_${index}` : this.focusedOptionId;
const element = DomHandler.findSingle(this.list, `li[id="${id}"]`);
if (element) {
element.scrollIntoView && element.scrollIntoView({ block: 'nearest', inline: 'start' });
}
},
autoUpdateModel() {
if (this.selectOnFocus && this.autoOptionFocus && !this.hasSelectedOption) {
this.focusedOptionInfo.index = this.findFirstFocusedOptionIndex();
const value = this.getOptionValue(this.visibleOptions[this.focusedOptionInfo.index]);
this.updateModel(null, value);
}
},
updateModel(event, value) {
this.$emit('update:modelValue', value);
this.$emit('change', { originalEvent: event, value });
},
createProcessedOptions(options, level = 0, parent = {}, parentKey = '') {
const processedOptions = [];
options && options.forEach((option, index) => {
const key = (parentKey !== '' ? parentKey + '_' : '') + index;
const newOption = {
option,
index,
level,
key,
parent,
parentKey
}
newOption['children'] = this.createProcessedOptions(this.getOptionGroupChildren(option, level), level + 1, newOption, key);
processedOptions.push(newOption);
});
return processedOptions;
},
overlayRef(el) {
this.overlay = el;
}
},
computed: {
containerClass() {
return ['p-cascadeselect p-component p-inputwrapper', {
'p-disabled': this.disabled,
'p-focus': this.focused,
'p-inputwrapper-filled': this.modelValue,
'p-inputwrapper-focus': this.focused || this.overlayVisible,
'p-overlay-open': this.overlayVisible
}];
},
labelClass() {
return ['p-cascadeselect-label', {
'p-placeholder': this.label === this.placeholder,
'p-cascadeselect-label-empty': !this.$slots['value'] && (this.label === 'p-emptylabel' || this.label.length === 0)
}];
},
panelStyleClass() {
return ['p-cascadeselect-panel p-component', this.panelClass, {
'p-input-filled': this.$primevue.config.inputStyle === 'filled',
'p-ripple-disabled': this.$primevue.config.ripple === false
}];
},
dropdownIconClass() {
return ['p-cascadeselect-trigger-icon', this.loading ? this.loadingIcon : 'pi pi-chevron-down'];
},
hasSelectedOption() {
return ObjectUtils.isNotEmpty(this.modelValue);
},
label() {
const label = this.placeholder || 'p-emptylabel';
if (this.hasSelectedOption) {
const activeOptionPath = this.findOptionPathByValue(this.modelValue);
const processedOption = activeOptionPath.length ? activeOptionPath[activeOptionPath.length - 1] : null;
return processedOption ? this.getOptionLabel(processedOption.option) : label;
}
return label;
},
processedOptions() {
return this.createProcessedOptions(this.options || []);
},
visibleOptions() {
const processedOption = this.activeOptionPath.find(p => p.key === this.focusedOptionInfo.parentKey);
return processedOption ? processedOption.children : this.processedOptions;
},
equalityKey() {
return this.optionValue ? null : this.dataKey;
},
searchResultMessageText() {
return ObjectUtils.isNotEmpty(this.visibleOptions) ? this.searchMessageText.replaceAll('{0}', this.visibleOptions.length) : this.emptySearchMessageText;
},
searchMessageText() {
return this.searchMessage || this.$primevue.config.locale.searchMessage || '';
},
emptySearchMessageText() {
return this.emptySearchMessage || this.$primevue.config.locale.emptySearchMessage || '';
},
emptyMessageText() {
return this.emptyMessage || this.$primevue.config.locale.emptyMessage || '';
},
selectionMessageText() {
return this.selectionMessage || this.$primevue.config.locale.selectionMessage || '';
},
emptySelectionMessageText() {
return this.emptySelectionMessage || this.$primevue.config.locale.emptySelectionMessage || '';
},
selectedMessageText() {
return this.hasSelectedOption ? this.selectionMessageText.replaceAll('{0}', '1') : this.emptySelectionMessageText;
},
focusedOptionId() {
return this.focusedOptionInfo.index !== -1 ? `${this.id}${ObjectUtils.isNotEmpty(this.focusedOptionInfo.parentKey) ? '_' + this.focusedOptionInfo.parentKey : ''}_${this.focusedOptionInfo.index}` : null;
}
},
components: {
'CascadeSelectSub': CascadeSelectSub,
'Portal': Portal
}
}
</script>
<style>
.p-cascadeselect {
display: inline-flex;
cursor: pointer;
position: relative;
user-select: none;
}
.p-cascadeselect-trigger {
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
}
.p-cascadeselect-label {
display: block;
white-space: nowrap;
overflow: hidden;
flex: 1 1 auto;
width: 1%;
text-overflow: ellipsis;
cursor: pointer;
}
.p-cascadeselect-label-empty {
overflow: hidden;
visibility: hidden;
}
.p-cascadeselect .p-cascadeselect-panel {
min-width: 100%;
}
.p-cascadeselect-panel {
position: absolute;
top: 0;
left: 0;
}
.p-cascadeselect-item {
cursor: pointer;
font-weight: normal;
white-space: nowrap;
}
.p-cascadeselect-item-content {
display: flex;
align-items: center;
overflow: hidden;
position: relative;
}
.p-cascadeselect-group-icon {
margin-left: auto;
}
.p-cascadeselect-items {
margin: 0;
padding: 0;
list-style-type: none;
min-width: 100%;
}
.p-fluid .p-cascadeselect {
display: flex;
}
.p-fluid .p-cascadeselect .p-cascadeselect-label {
width: 1%;
}
.p-cascadeselect-sublist {
position: absolute;
min-width: 100%;
z-index: 1;
display: none;
}
.p-cascadeselect-item-active {
overflow: visible !important;
}
.p-cascadeselect-item-active > .p-cascadeselect-sublist {
display: block;
left: 100%;
top: 0;
}
</style>

View File

@ -0,0 +1,101 @@
<template>
<ul class="p-cascadeselect-panel p-cascadeselect-items">
<template v-for="(processedOption,index) of options" :key="getOptionLabelToRender(processedOption)">
<li :id="getOptionId(processedOption)" :class="['p-cascadeselect-item', {'p-cascadeselect-item-group': isOptionGroup(processedOption), 'p-cascadeselect-item-active p-highlight': isOptionActive(processedOption), 'p-focus': isOptionFocused(processedOption), 'p-disabled': isOptionDisabled(processedOption)}]"
role="treeitem" :aria-label="getOptionLabelToRender(processedOption)" :aria-selected="isOptionGroup(processedOption) ? undefined : isOptionSelected(processedOption)" :aria-expanded="isOptionGroup(processedOption) ? isOptionActive(processedOption) : undefined"
:aria-setsize="processedOption.length" :aria-posinset="index + 1" :aria-level="level + 1">
<div class="p-cascadeselect-item-content" @click="onOptionClick($event, processedOption)" v-ripple>
<component v-if="templates['option']" :is="templates['option']" :option="processedOption.option" />
<span v-else class="p-cascadeselect-item-text">{{getOptionLabelToRender(processedOption)}}</span>
<span v-if="isOptionGroup(processedOption)" class="p-cascadeselect-group-icon pi pi-angle-right" aria-hidden="true"></span>
</div>
<CascadeSelectSub v-if="isOptionGroup(processedOption) && isOptionActive(processedOption)" role="group" class="p-cascadeselect-sublist" :selectId="selectId" :focusedOptionId="focusedOptionId"
:options="getOptionGroupChildren(processedOption)" :activeOptionPath="activeOptionPath" :level="level + 1" :templates="templates" :optionLabel="optionLabel" :optionValue="optionValue" :optionDisabled="optionDisabled"
:optionGroupLabel="optionGroupLabel" :optionGroupChildren="optionGroupChildren" @option-change="onOptionChange" />
</li>
</template>
</ul>
</template>
<script>
import {ObjectUtils,DomHandler} from 'primevue/utils';
import Ripple from 'primevue/ripple';
export default {
name: 'CascadeSelectSub',
emits: ['option-change'],
props: {
selectId: String,
focusedOptionId: String,
options: Array,
optionLabel: String,
optionValue: String,
optionDisabled: null,
optionGroupLabel: String,
optionGroupChildren: Array,
activeOptionPath: Array,
level: Number,
templates: null
},
mounted() {
if (ObjectUtils.isNotEmpty(this.parentKey)) {
this.position();
}
},
methods: {
getOptionId(processedOption) {
return `${this.selectId}_${processedOption.key}`;
},
getOptionLabel(processedOption) {
return this.optionLabel ? ObjectUtils.resolveFieldData(processedOption.option, this.optionLabel) : processedOption.option;
},
getOptionValue(processedOption) {
return this.optionValue ? ObjectUtils.resolveFieldData(processedOption.option, this.optionValue) : processedOption.option;
},
isOptionDisabled(processedOption) {
return this.optionDisabled ? ObjectUtils.resolveFieldData(processedOption.option, this.optionDisabled) : false;
},
getOptionGroupLabel(processedOption) {
return this.optionGroupLabel ? ObjectUtils.resolveFieldData(processedOption.option, this.optionGroupLabel) : null;
},
getOptionGroupChildren(processedOption) {
return processedOption.children;
},
isOptionGroup(processedOption) {
return ObjectUtils.isNotEmpty(processedOption.children);
},
isOptionSelected(processedOption) {
return !this.isOptionGroup(processedOption) && this.isOptionActive(processedOption);
},
isOptionActive(processedOption) {
return this.activeOptionPath.some(path => path.key === processedOption.key);
},
isOptionFocused(processedOption) {
return this.focusedOptionId === this.getOptionId(processedOption);
},
getOptionLabelToRender(processedOption) {
return this.isOptionGroup(processedOption) ? this.getOptionGroupLabel(processedOption) : this.getOptionLabel(processedOption);
},
onOptionClick(event, processedOption) {
this.$emit('option-change', { originalEvent: event, processedOption, isFocus: true });
},
onOptionChange(event) {
this.$emit('option-change', event);
},
position() {
const parentItem = this.$el.parentElement;
const containerOffset = DomHandler.getOffset(parentItem);
const viewport = DomHandler.getViewport();
const sublistWidth = this.$el.offsetParent ? this.$el.offsetWidth : DomHandler.getHiddenElementOuterWidth(this.$el);
const itemOuterWidth = DomHandler.getOuterWidth(parentItem.children[0]);
if ((parseInt(containerOffset.left, 10) + itemOuterWidth + sublistWidth) > (viewport.width - DomHandler.calculateScrollbarWidth())) {
this.$el.style.left = '-100%';
}
}
},
directives: {
'ripple': Ripple
}
}
</script>

View File

@ -0,0 +1,9 @@
{
"main": "./cascadeselect.cjs.js",
"module": "./cascadeselect.esm.js",
"unpkg": "./cascadeselect.min.js",
"types": "./CascadeSelect.d.ts",
"browser": {
"./sfc": "./CascadeSelect.vue"
}
}

109
components/chart/Chart.d.ts vendored Executable file
View File

@ -0,0 +1,109 @@
import { ClassComponent, GlobalComponentConstructor } from '../ts-helpers';
export interface ChartSelectEvent {
/**
* Browser event.
*/
originalEvent: Event;
/**
* Selected element.
*/
element: HTMLElement | any;
/**
* Selected dataset.
*/
dataset: any;
}
export interface ChartProps {
/**
* Type of the chart.
*/
type?: string | undefined;
/**
* Data to display.
*/
data?: object | undefined;
/**
* Options to customize the chart.
*/
options?: object | undefined;
/**
* Used to custom plugins of the chart.
*/
plugins?: any;
/**
* Width of the chart in non-responsive mode.
* Default value is 300.
*/
width?: number | undefined;
/**
* Height of the chart in non-responsive mode.
* Default value is 150.
*/
height?: number | undefined;
}
export interface ChartSlots {
}
export declare type ChartEmits = {
/**
* Callback to invoke when a tab gets expanded.
* @param {ChartSelectEvent} event - Custom select event.
*/
'select': (event: ChartSelectEvent) => void;
/**
* Callback to invoke when chart is loaded.
* @param {*} chart - Chart instance.
*/
'loaded': (chart: any) => void;
}
declare class Chart extends ClassComponent<ChartProps, ChartSlots, ChartEmits> {
/**
* Redraws the graph.
*
* @memberof Chart
*/
refresh: () => void;
/**
* Destroys the graph first and then creates it again.
*
* @memberof Chart
*/
reinit: () => void;
/**
* Returns an HTML string of a legend for that chart. The legend is generated from the legendCallback in the options.
*
* @memberof Chart
*/
generateLegend: () => string | any;
/**
* Returns Chart instance.
*
* @memberof Chart
*/
getChart: () => any;
}
declare module '@vue/runtime-core' {
interface GlobalComponents {
Chart: GlobalComponentConstructor<Chart>
}
}
/**
*
* Chart components are based on Charts.js, an open source HTML5 based charting library.
*
* Helper API;
*
* - [Chart.js](https://www.chartjs.org/)
*
* Demos:
*
* - [Chart](https://www.primefaces.org/primevue/showcase/#/chart)
*
*/
export default Chart;

113
components/chart/Chart.vue Executable file
View File

@ -0,0 +1,113 @@
<template>
<div class="p-chart">
<canvas ref="canvas" :width="width" :height="height" @click="onCanvasClick($event)"></canvas>
</div>
</template>
<script>
export default {
name: 'Chart',
emits: ['select', 'loaded'],
props: {
type: String,
data: null,
options: null,
plugins: null,
width: {
type: Number,
default: 300
},
height: {
type: Number,
default: 150
},
},
chart: null,
mounted() {
this.initChart();
},
beforeUnmount() {
if (this.chart) {
this.chart.destroy();
this.chart = null;
}
},
watch: {
/*
* Use deep watch to enable triggering watch for changes within structure
* otherwise the entire data object needs to be replaced to trigger watch
*/
data: {
handler() {
this.reinit();
},
deep: true
},
type() {
this.reinit();
},
options() {
this.reinit();
}
},
methods: {
initChart() {
import('chart.js/auto').then((module) => {
if (this.chart) {
this.chart.destroy();
this.chart = null;
}
if (module && module.default) {
this.chart = new module.default(this.$refs.canvas, {
type: this.type,
data: this.data,
options: this.options,
plugins: this.plugins
});
}
this.$emit('loaded', this.chart);
});
},
getCanvas() {
return this.$canvas;
},
getChart() {
return this.chart;
},
getBase64Image() {
return this.chart.toBase64Image();
},
refresh() {
if (this.chart) {
this.chart.update();
}
},
reinit() {
this.initChart();
},
onCanvasClick(event) {
if (this.chart) {
const element = this.chart.getElementsAtEventForMode(event, 'nearest', { intersect: true }, false);
const dataset = this.chart.getElementsAtEventForMode(event, 'dataset', { intersect: true }, false);
if (element && element[0] && dataset) {
this.$emit('select', {originalEvent: event, element: element[0], dataset: dataset});
}
}
},
generateLegend() {
if (this.chart) {
return this.chart.generateLegend();
}
}
}
}
</script>
<style>
.p-chart {
position: relative;
}
</style>

View File

@ -0,0 +1,9 @@
{
"main": "./chart.cjs.js",
"module": "./chart.esm.js",
"unpkg": "./chart.min.js",
"types": "./Chart.d.ts",
"browser": {
"./sfc": "./Chart.vue"
}
}

View File

@ -0,0 +1,17 @@
.p-checkbox {
display: inline-flex;
cursor: pointer;
user-select: none;
vertical-align: bottom;
position: relative;
}
.p-checkbox.p-checkbox-disabled {
cursor: default;
}
.p-checkbox-box {
display: flex;
justify-content: center;
align-items: center;
}

114
components/checkbox/Checkbox.d.ts vendored Executable file
View File

@ -0,0 +1,114 @@
import { InputHTMLAttributes } from 'vue';
import { ClassComponent, GlobalComponentConstructor } from '../ts-helpers';
export interface CheckboxProps {
/**
* Value of the checkbox.
*/
value?: any;
/**
* Value binding of the checkbox.
*/
modelValue?: any;
/**
* Name of the input element.
*/
name?: string | undefined;
/**
* Allows to select a boolean value instead of multiple values.
*/
binary?: boolean;
/**
* When present, it specifies that the element should be disabled.
*/
disabled?: boolean | undefined;
/**
* When present, it specifies that an input field is read-only.
*/
readonly?: boolean | undefined;
/**
* When present, it specifies that the element is required.
*/
required?: boolean | undefined;
/**
* Index of the element in tabbing order.
*/
tabindex?: number | undefined;
/**
* Value in checked state.
*/
trueValue?: any;
/**
* Value in unchecked state.
*/
falseValue?: any;
/**
* Identifier of the underlying input element.
*/
inputId?: string | undefined;
/**
* Style class of the input field.
*/
inputClass?: any | undefined;
/**
* Inline style of the input field.
*/
inputStyle?: any | undefined;
/**
* Uses to pass all properties of the HTMLInputElement to the focusable input element inside the component.
*/
inputProps?: InputHTMLAttributes | undefined;
/**
* Establishes relationships between the component and label(s) where its value should be one or more element IDs.
*/
'aria-labelledby'?: string | undefined;
/**
* Establishes a string value that labels the component.
*/
'aria-label'?: string | undefined;
}
export interface CheckboxSlots {
}
export declare type CheckboxEmits = {
/**
* Emitted when the page changes.
* @param {*} value - New page value.
*/
'update:page': (value: any) => void;
/**
* Callback to invoke on value click.
* @param {MouseEvent} event - Browser event.
*/
'click': (event: MouseEvent) => void;
/**
* Callback to invoke on value change.
* @param {Event} event - Browser event.
*/
'change': (event: Event) => void;
/**
* Callback to invoke on value change.
* @param {boolean} value - New value.
*/
'input': (value: boolean) => void;
}
declare class Checkbox extends ClassComponent<CheckboxProps, CheckboxSlots, CheckboxEmits> { }
declare module '@vue/runtime-core' {
interface GlobalComponents {
Checkbox: GlobalComponentConstructor<Checkbox>
}
}
/**
*
* Checkbox is an extension to standard checkbox element with theming.
*
* Demos:
*
* - [Checkbox](https://www.primefaces.org/primevue/showcase/#/checkbox)
*
*/
export default Checkbox;

View File

@ -0,0 +1,28 @@
import { mount } from '@vue/test-utils';
import Checkbox from './Checkbox.vue';
describe('Checkbox.vue', () => {
let wrapper;
beforeEach(() => {
wrapper = mount(Checkbox, {
props: {
modelValue: false,
binary: true
}
});
});
it('should exist', () => {
expect(wrapper.find('.p-checkbox.p-component').exists()).toBe(true);
expect(wrapper.find('.p-checkbox-icon.pi.pi-check').exists()).toBe(false);
});
it('should exist', async () => {
await wrapper.setProps({ modelValue: true });
expect(wrapper.find('.p-checkbox-checked').exists()).toBe(true);
expect(wrapper.find('.p-checkbox-box.p-highlight').exists()).toBe(true);
expect(wrapper.find('.p-checkbox-icon.pi.pi-check').exists()).toBe(true);
});
});

114
components/checkbox/Checkbox.vue Executable file
View File

@ -0,0 +1,114 @@
<template>
<div :class="containerClass" @click="onClick($event)">
<div class="p-hidden-accessible">
<input :id="inputId" ref="input" type="checkbox" :value="value" :class="inputClass" :style="inputStyle" :name="name" :checked="checked" :tabindex="tabindex" :disabled="disabled" :readonly="readonly" :required="required" :aria-labelledby="ariaLabelledby" :aria-label="ariaLabel"
@focus="onFocus($event)" @blur="onBlur($event)" v-bind="inputProps">
</div>
<div ref="box" :class="['p-checkbox-box', {'p-highlight': checked, 'p-disabled': disabled, 'p-focus': focused}]">
<span :class="['p-checkbox-icon', {'pi pi-check': checked}]"></span>
</div>
</div>
</template>
<script>
import {ObjectUtils} from 'primevue/utils';
export default {
name: 'Checkbox',
emits: ['click', 'update:modelValue', 'change', 'input', 'focus', 'blur'],
props: {
value: null,
modelValue: null,
binary: Boolean,
name: {
type: String,
default: null
},
trueValue: {
type: null,
default: true
},
falseValue: {
type: null,
default: false
},
disabled: {
type: Boolean,
default: false
},
readonly: {
type: Boolean,
default: false
},
required: {
type: Boolean,
default: false
},
tabindex: {
type: Number,
default: null
},
inputId: null,
inputClass: null,
inputStyle: null,
inputProps: null,
'aria-labelledby': {
type: String,
default: null
},
'aria-label': {
type: String,
default: null
}
},
data() {
return {
focused: false
}
},
methods: {
onClick(event) {
if (!this.disabled) {
let newModelValue;
if (this.binary) {
newModelValue = this.checked ? this.falseValue : this.trueValue;
}
else {
if (this.checked)
newModelValue = this.modelValue.filter(val => !ObjectUtils.equals(val, this.value));
else
newModelValue = this.modelValue ? [...this.modelValue, this.value] : [this.value];
}
this.$emit('click', event);
this.$emit('update:modelValue', newModelValue);
this.$emit('change', event);
this.$emit('input', newModelValue);
this.$refs.input.focus();
}
},
onFocus(event) {
this.focused = true;
this.$emit('focus', event);
},
onBlur(event) {
this.focused = false;
this.$emit('blur', event);
}
},
computed: {
checked() {
return this.binary ? this.modelValue === this.trueValue : ObjectUtils.contains(this.value, this.modelValue);
},
containerClass() {
return [
'p-checkbox p-component', {
'p-checkbox-checked': this.checked,
'p-checkbox-disabled': this.disabled,
'p-checkbox-focused': this.focused
}];
}
}
}
</script>

View File

@ -0,0 +1,9 @@
{
"main": "./checkbox.cjs.js",
"module": "./checkbox.esm.js",
"unpkg": "./checkbox.min.js",
"types": "./Checkbox.d.ts",
"browser": {
"./sfc": "./Checkbox.vue"
}
}

60
components/chip/Chip.d.ts vendored Normal file
View File

@ -0,0 +1,60 @@
import { VNode } from 'vue';
import { ClassComponent, GlobalComponentConstructor } from '../ts-helpers';
export interface ChipProps {
/**
* Defines the text to display.
*/
label?: string;
/**
* Defines the icon to display.
*/
icon?: string;
/**
* Defines the image to display.
*/
image?: string;
/**
* Whether to display a remove icon.
*/
removable?: boolean;
/**
* Icon of the remove element.
* Default value is 'pi pi-times-circle'.
*/
removeIcon?: string;
}
export interface ChipSlots {
/**
* Content can easily be customized with the default slot instead of using the built-in modes.
*/
default: () => VNode[];
}
export declare type ChipEmits = {
/**
* Callback to invoke when a chip is removed.
* @param {Event} event - Browser event.
*/
'remove': (event: Event) => void;
}
declare class Chip extends ClassComponent<ChipProps, ChipSlots, ChipEmits> { }
declare module '@vue/runtime-core' {
interface GlobalComponents {
Chip: GlobalComponentConstructor<Chip>
}
}
/**
*
* Chip represents entities using icons, labels and images.
*
* Demos:
*
* - [Chip](https://www.primefaces.org/primevue/showcase/#/chip)
*
*/
export default Chip;

View File

@ -0,0 +1,29 @@
import { mount } from '@vue/test-utils';
import Chip from './Chip.vue';
describe('Chip.vue', () => {
let wrapper;
beforeEach(() => {
wrapper = mount(Chip, {
props: {
label: 'PrimeVue',
icon: 'pi pi-primevue',
removable: true
}
});
});
it('should exists', () => {
expect(wrapper.find('.p-chip.p-component').exists()).toBe(true);
expect(wrapper.find('.p-chip-icon').classes()).toContain('pi-primevue');
expect(wrapper.find('.p-chip-text').text()).toBe('PrimeVue');
expect(wrapper.find('.p-chip-remove-icon').exists()).toBe(true);
});
it('should close icon work', async () => {
await wrapper.find('.p-chip-remove-icon').trigger('click')
expect(wrapper.find('.p-chip.p-component').exists()).toBe(false);
});
});

88
components/chip/Chip.vue Normal file
View File

@ -0,0 +1,88 @@
<template>
<div :class="containerClass" v-if="visible">
<slot>
<img :src="image" v-if="image">
<span :class="iconClass" v-else-if="icon"></span>
<div class="p-chip-text" v-if="label">{{label}}</div>
</slot>
<span v-if="removable" tabindex="0" :class="removeIconClass"
@click="close" @keydown.enter="close"></span>
</div>
</template>
<script>
export default {
name: 'Chip',
emits: ['remove'],
props: {
label: {
type: String,
default: null
},
icon: {
type: String,
default: null
},
image: {
type: String,
default: null
},
removable: {
type: Boolean,
default: false
},
removeIcon: {
type: String,
default: 'pi pi-times-circle'
}
},
data() {
return {
visible: true
}
},
methods: {
close(event) {
this.visible = false;
this.$emit('remove', event);
}
},
computed: {
containerClass() {
return ['p-chip p-component', {
'p-chip-image': this.image != null
}];
},
iconClass() {
return ['p-chip-icon', this.icon];
},
removeIconClass() {
return ['p-chip-remove-icon', this.removeIcon];
}
}
}
</script>
<style>
.p-chip {
display: inline-flex;
align-items: center;
}
.p-chip-text {
line-height: 1.5;
}
.p-chip-icon.pi {
line-height: 1.5;
}
.p-chip-remove-icon {
line-height: 1.5;
cursor: pointer;
}
.p-chip img {
border-radius: 50%;
}
</style>

View File

@ -0,0 +1,9 @@
{
"main": "./chip.cjs.js",
"module": "./chip.esm.js",
"unpkg": "./chip.min.js",
"types": "./Chip.d.ts",
"browser": {
"./sfc": "./Chip.vue"
}
}

124
components/chips/Chips.d.ts vendored Executable file
View File

@ -0,0 +1,124 @@
import { InputHTMLAttributes, VNode } from 'vue';
import { ClassComponent, GlobalComponentConstructor } from '../ts-helpers';
export interface ChipsAddEvent {
/**
* Browser event.
*/
originalEvent: Event;
/**
* Added/Removed item value.
*/
value: any;
}
/**
* @extends ChipsAddEvent
*/
export interface ChipsRemoveEvent extends ChipsAddEvent { }
export interface ChipsProps {
/**
* Value of the component.
*/
modelValue?: any[];
/**
* Maximum number of entries allowed.
*/
max?: number | undefined;
/**
* Whether to add an item when the input loses focus.
*/
addOnBlur?: boolean | undefined;
/**
* Whether to allow duplicate values or not.
* Default value is true.
*/
allowDuplicate?: boolean | undefined;
/**
* Separator char to add an item when pressed in addition to the enter key. Currently only possible value is ','
*/
separator?: string | undefined;
/**
* Identifier of the focus input to match a label defined for the chips.
*/
inputId?: string | undefined;
/**
* Style class of the input field.
*/
inputClass?: any | undefined;
/**
* Inline style of the input field.
*/
inputStyle?: any | undefined;
/**
* Uses to pass all properties of the HTMLInputElement to the focusable input element inside the component.
*/
inputProps?: InputHTMLAttributes | undefined;
/**
* When present, it specifies that the element should be disabled.
*/
disabled?: boolean | undefined;
/**
* Placeholder text for the input.
*/
placeholder?: string | undefined;
/**
* Establishes relationships between the component and label(s) where its value should be one or more element IDs.
*/
'aria-labelledby'?: string | undefined;
/**
* Establishes a string value that labels the component.
*/
'aria-label'?: string | undefined;
}
export interface ChipsSlots {
/**
* Custom chip template.
* @param {Object} scope - chip slot's params.
*/
chip: (scope: {
/**
* Value of the component
*/
value: any;
}) => VNode[];
}
export declare type ChipsEmits = {
/**
* Emitted when the value changes.
* @param {*} value - New value.
*/
'update:modelValue': (value: any[]) => void;
/**
* Callback to invoke when a chip is added.
* @param {ChipsAddEvent} event - Custom add event.
*/
'add': (event: ChipsAddEvent) => void;
/**
* Callback to invoke when a chip is removed.
* @param {ChipsRemoveEvent} event - Custom remove event.
*/
'remove': (event: ChipsRemoveEvent) => void;
}
declare class Chips extends ClassComponent<ChipsProps, ChipsSlots, ChipsEmits> { }
declare module '@vue/runtime-core' {
interface GlobalComponents {
Chips: GlobalComponentConstructor<Chips>
}
}
/**
*
* Chips is used to enter multiple values on an input field.
*
* Demos:
*
* - [Chips](https://www.primefaces.org/primevue/showcase/#/chips)
*
*/
export default Chips;

View File

@ -0,0 +1,33 @@
import { mount } from '@vue/test-utils';
import Chips from './Chips.vue';
describe('Chips.vue', () => {
let wrapper;
beforeEach(() => {
wrapper = mount(Chips, {
props: {
modelValue: null
}
});
});
it('should exist', () => {
expect(wrapper.find('.p-chips.p-component.p-inputwrapper').exists()).toBe(true);
expect(wrapper.find('ul.p-chips-multiple-container').exists()).toBe(true);
expect(wrapper.find('li.p-chips-input-token').exists()).toBe(true);
});
it('should add item', async() => {
const addItem = jest.spyOn(wrapper.vm, 'addItem');
await wrapper.vm.addItem({},'PrimeVue', false);
await wrapper.setProps({ modelValue: ['PrimeVue'] });
expect(addItem).toHaveBeenCalled();
expect(wrapper.findAll('.p-chips-token').length).toBe(1);
expect(wrapper.find('.p-chips-token-label').exists()).toBe(true);
expect(wrapper.find('.p-chips-token-label').text()).toBe('PrimeVue');
});
});

298
components/chips/Chips.vue Executable file
View File

@ -0,0 +1,298 @@
<template>
<div :class="containerClass">
<ul ref="container" class="p-inputtext p-chips-multiple-container" tabindex="-1" role="listbox" aria-orientation="horizontal" :aria-labelledby="ariaLabelledby" :aria-label="ariaLabel" :aria-activedescendant="focused ? focusedOptionId : undefined"
@click="onWrapperClick()" @focus="onContainerFocus" @blur="onContainerBlur" @keydown="onContainerKeyDown">
<li v-for="(val,i) of modelValue" :key="`${i}_${val}`" :id="id + '_chips_item_' + i" role="option" :class="['p-chips-token', {'p-focus': focusedIndex === i}]"
:aria-label="val" :aria-selected="true" :aria-setsize="modelValue.length" :aria-posinset="i + 1">
<slot name="chip" :value="val">
<span class="p-chips-token-label">{{val}}</span>
</slot>
<span class="p-chips-token-icon pi pi-times-circle" @click="removeItem($event, i)" aria-hidden="true"></span>
</li>
<li class="p-chips-input-token" role="option">
<input ref="input" type="text" :id="inputId" :class="inputClass" :style="inputStyle" :disabled="disabled || maxedOut" :placeholder="placeholder"
@focus="onFocus($event)" @blur="onBlur($event)" @input="onInput" @keydown="onKeyDown($event)" @paste="onPaste($event)" v-bind="inputProps">
</li>
</ul>
</div>
</template>
<script>
import {UniqueComponentId} from 'primevue/utils';
export default {
name: 'Chips',
emits: ['update:modelValue', 'add', 'remove', 'focus', 'blur'],
props: {
modelValue: {
type: Array,
default: null
},
max: {
type: Number,
default: null
},
separator: {
type: String,
default: null
},
addOnBlur: {
type: Boolean,
default: null
},
allowDuplicate: {
type: Boolean,
default: true
},
placeholder: {
type: String,
default: null
},
inputId: null,
inputClass: null,
inputStyle: null,
inputProps: null,
disabled: {
type: Boolean,
default: false
},
'aria-labelledby': {
type: String,
default: null
},
'aria-label': {
type: String,
default: null
}
},
data() {
return {
id: UniqueComponentId(),
inputValue: null,
focused: false,
focusedIndex: null
};
},
methods: {
onWrapperClick() {
this.$refs.input.focus();
},
onInput(event) {
this.inputValue = event.target.value;
this.focusedIndex = null;
},
onFocus(event) {
this.focused = true;
this.focusedIndex = null;
this.$emit('focus', event);
},
onBlur(event) {
this.focused = false;
this.focusedIndex = null;
if (this.addOnBlur) {
this.addItem(event, event.target.value, false);
}
this.$emit('blur', event);
},
onKeyDown(event) {
const inputValue = event.target.value;
switch(event.code) {
case 'Backspace':
if (inputValue.length === 0 && this.modelValue && this.modelValue.length > 0) {
if (this.focusedIndex !== null) {
this.removeItem(event, this.focusedIndex);
}
else this.removeItem(event, this.modelValue.length - 1);
}
break;
case 'Enter':
if (inputValue && inputValue.trim().length && !this.maxedOut) {
this.addItem(event, inputValue, true);
}
break;
case 'ArrowLeft':
if (inputValue.length === 0 && this.modelValue && this.modelValue.length > 0) {
this.$refs.container.focus();
}
break;
case 'ArrowRight':
event.stopPropagation();
break;
default:
if (this.separator) {
if (this.separator === ',' && event.key === ',') {
this.addItem(event, inputValue, true);
}
}
break;
}
},
onPaste(event) {
if (this.separator) {
let pastedData = (event.clipboardData || window['clipboardData']).getData('Text');
if (pastedData) {
let value = this.modelValue || [];
let pastedValues = pastedData.split(this.separator);
pastedValues = pastedValues.filter(val => (this.allowDuplicate || value.indexOf(val) === -1));
value = [...value, ...pastedValues];
this.updateModel(event, value, true);
}
}
},
onContainerFocus() {
this.focused = true;
},
onContainerBlur() {
this.focusedIndex = -1;
this.focused = false;
},
onContainerKeyDown(event) {
switch (event.code) {
case 'ArrowLeft':
this.onArrowLeftKeyOn(event);
break;
case 'ArrowRight':
this.onArrowRightKeyOn(event);
break;
case 'Backspace':
this.onBackspaceKeyOn(event);
break;
default:
break;
}
},
onArrowLeftKeyOn() {
if (this.inputValue.length === 0 && this.modelValue && this.modelValue.length > 0) {
this.focusedIndex = this.focusedIndex === null ? this.modelValue.length - 1 : this.focusedIndex - 1;
if (this.focusedIndex < 0) this.focusedIndex = 0;
}
},
onArrowRightKeyOn() {
if (this.inputValue.length === 0 && this.modelValue && this.modelValue.length > 0) {
if (this.focusedIndex === this.modelValue.length - 1) {
this.focusedIndex = null;
this.$refs.input.focus();
}
else {
this.focusedIndex++;
}
}
},
onBackspaceKeyOn(event) {
if (this.focusedIndex !== null) {
this.removeItem(event, this.focusedIndex);
}
},
updateModel(event, value, preventDefault) {
this.$emit('update:modelValue', value);
this.$emit('add', {
originalEvent: event,
value: value
});
this.$refs.input.value = '';
this.inputValue = '';
if (preventDefault) {
event.preventDefault();
}
},
addItem(event, item, preventDefault) {
if (item && item.trim().length) {
let value = this.modelValue ? [...this.modelValue]: [];
if (this.allowDuplicate || value.indexOf(item) === -1) {
value.push(item);
this.updateModel(event, value, preventDefault);
}
}
},
removeItem(event, index) {
if (this.disabled) {
return;
}
let values = [...this.modelValue];
const removedItem = values.splice(index, 1);
this.focusedIndex = null;
this.$refs.input.focus();
this.$emit('update:modelValue', values);
this.$emit('remove', {
originalEvent: event,
value: removedItem
});
}
},
computed: {
maxedOut() {
return this.max && this.modelValue && this.max === this.modelValue.length;
},
containerClass() {
return ['p-chips p-component p-inputwrapper', {
'p-disabled': this.disabled,
'p-focus': this.focused,
'p-inputwrapper-filled': ((this.modelValue && this.modelValue.length) || (this.inputValue && this.inputValue.length)),
'p-inputwrapper-focus': this.focused
}];
},
focusedOptionId() {
return this.focusedIndex !== null ? `${this.id}_chips_item_${this.focusedIndex}` : null;
}
}
}
</script>
<style>
.p-chips {
display: inline-flex;
}
.p-chips-multiple-container {
margin: 0;
padding: 0;
list-style-type: none;
cursor: text;
overflow: hidden;
display: flex;
align-items: center;
flex-wrap: wrap;
}
.p-chips-token {
cursor: default;
display: inline-flex;
align-items: center;
flex: 0 0 auto;
}
.p-chips-input-token {
flex: 1 1 auto;
display: inline-flex;
}
.p-chips-token-icon {
cursor: pointer;
}
.p-chips-input-token input {
border: 0 none;
outline: 0 none;
background-color: transparent;
margin: 0;
padding: 0;
box-shadow: none;
border-radius: 0;
width: 100%;
}
.p-fluid .p-chips {
display: flex;
}
</style>

View File

@ -0,0 +1,9 @@
{
"main": "./chips.cjs.js",
"module": "./chips.esm.js",
"unpkg": "./chips.min.js",
"types": "./Chips.d.ts",
"browser": {
"./sfc": "./Chips.vue"
}
}

View File

@ -0,0 +1,7 @@
.p-colorpicker-panel .p-colorpicker-color {
background: transparent url("./images/color.png") no-repeat left top;
}
.p-colorpicker-panel .p-colorpicker-hue {
background: transparent url("./images/hue.png") no-repeat left top;
}

109
components/colorpicker/ColorPicker.d.ts vendored Executable file
View File

@ -0,0 +1,109 @@
import { ClassComponent, GlobalComponentConstructor } from '../ts-helpers';
type ColorPickerFormatType = 'hex' | 'rgb' | 'hsb' | undefined;
type ColorPickerAppendToType = 'body' | 'self' | string | undefined | HTMLElement;
export interface ColorPickerChangeEvent {
/**
* Browser event
*/
originalEvent: Event;
/**
* Selected color value.
*/
value: any;
}
export interface ColorPickerProps {
/**
* Value of the component.
*/
modelValue?: any;
/**
* Initial color to display when value is not defined.
* Default value is ff0000.
*/
defaultColor?: any;
/**
* Whether to display as an overlay or not.
*/
inline?: boolean | undefined;
/**
* Format to use in value binding, supported formats are 'hex', 'rgb' and 'hsb'.
* @see ColorPickerFormatType
* Default value is 'hex'.
*/
format?: ColorPickerFormatType;
/**
* When present, it specifies that the component should be disabled.
*/
disabled?: boolean | undefined;
/**
* Index of the element in tabbing order.
*/
tabindex?: string | undefined;
/**
* Whether to automatically manage layering.
* Default value is true.
*/
autoZIndex?: boolean | undefined;
/**
* Base zIndex value to use in layering.
* Default value is 0.
*/
baseZIndex?: number | undefined;
/**
* Style class of the overlay panel.
*/
panelClass?: any;
/**
* A valid query selector or an HTMLElement to specify where the overlay gets attached. Special keywords are 'body' for document body and 'self' for the element itself.
* @see ColorPickerAppendToType
* Default value is 'body'.
*/
appendTo?: ColorPickerAppendToType;
}
export interface ColorPickerSlots {
}
export declare type ColorPickerEmits = {
/**
* Emitted when the value changes.
* @param {*} value - New value.
*/
'update:modelValue': (value: any) => void;
/**
* Callback to invoke when a chip is added.
* @param {ColorPickerChangeEvent} event - Custom add event.
*/
'change': (event: ColorPickerChangeEvent) => void;
/**
* Callback to invoke when input is cleared by the user.
*/
'show': () => void;
/**
* Callback to invoke when input is cleared by the user.
*/
'hide': () => void;
}
declare class ColorPicker extends ClassComponent<ColorPickerProps, ColorPickerSlots, ColorPickerEmits> { }
declare module '@vue/runtime-core' {
interface GlobalComponents {
ColorPicker: GlobalComponentConstructor<ColorPicker>
}
}
/**
*
* ColorPicker is an input component to select a color.
*
* Demos:
*
* - [ColorPicker](https://www.primefaces.org/primevue/showcase/#/colorpicker)
*
*/
export default ColorPicker;

View File

@ -0,0 +1,59 @@
import { mount } from '@vue/test-utils';
import PrimeVue from '@/components/config/PrimeVue';
import ColorPicker from './ColorPicker.vue';
describe('ColorPicker.vue', () => {
let wrapper;
beforeEach(() => {
wrapper = mount(ColorPicker, {
global: {
plugins: [PrimeVue],
stubs: {
teleport: true
}
},
props: {
modelValue: null
}
});
});
it('should exist', () => {
expect(wrapper.find('.p-colorpicker.p-component').exists()).toBe(true);
expect(wrapper.find('.p-colorpicker-preview.p-inputtext').exists()).toBe(true);
});
it('should input click triggered', async() => {
const input = wrapper.find('.p-colorpicker-preview.p-inputtext');
const onInputClick = jest.spyOn(wrapper.vm, 'onInputClick');
await input.trigger('click');
expect(onInputClick).toHaveBeenCalled();
expect(wrapper.find('.p-colorpicker-panel').exists()).toBe(true);
expect(wrapper.find('.p-colorpicker-color-selector').exists()).toBe(true);
expect(wrapper.find('.p-colorpicker-hue').exists()).toBe(true);
});
it('should mouse events triggered', async() => {
const input = wrapper.find('.p-colorpicker-preview.p-inputtext');
await input.trigger('click');
const onColorMousedown = jest.spyOn(wrapper.vm, 'onColorMousedown');
const onHueMousedown = jest.spyOn(wrapper.vm, 'onHueMousedown');
const event = { pageX: 100, pageY: 120, preventDefault: () => {}};
const event2 = { pageX: 70, pageY: 20, preventDefault: () => {}};
wrapper.vm.onColorMousedown(event);
expect(onColorMousedown).toHaveBeenCalled();
expect(wrapper.find('.p-colorpicker-preview.p-inputtext').element.style.backgroundColor).not.toBe('rgb(255, 0, 0)');
wrapper.vm.onHueMousedown(event2);
expect(onHueMousedown).toHaveBeenCalled();
expect(wrapper.find('.p-colorpicker-preview.p-inputtext').element.style.backgroundColor).not.toBe('rgb(255, 0, 0)');
});
});

View File

@ -0,0 +1,670 @@
<template>
<div ref="container" :class="containerClass">
<input ref="input" type="text" :class="inputClass" readonly="readonly" :tabindex="tabindex" :disabled="disabled"
@click="onInputClick" @keydown="onInputKeydown" v-if="!inline"/>
<Portal :appendTo="appendTo" :disabled="inline">
<transition name="p-connected-overlay" @enter="onOverlayEnter" @leave="onOverlayLeave" @after-leave="onOverlayAfterLeave">
<div :ref="pickerRef" :class="pickerClass" v-if="inline ? true : overlayVisible" @click="onOverlayClick">
<div class="p-colorpicker-content">
<div :ref="colorSelectorRef" class="p-colorpicker-color-selector" @mousedown="onColorMousedown($event)"
@touchstart="onColorDragStart($event)" @touchmove="onDrag($event)" @touchend="onDragEnd()">
<div class="p-colorpicker-color">
<div :ref="colorHandleRef" class="p-colorpicker-color-handle"></div>
</div>
</div>
<div :ref="hueViewRef" class="p-colorpicker-hue" @mousedown="onHueMousedown($event)"
@touchstart="onHueDragStart($event)" @touchmove="onDrag($event)" @touchend="onDragEnd()">
<div :ref="hueHandleRef" class="p-colorpicker-hue-handle"></div>
</div>
</div>
</div>
</transition>
</Portal>
</div>
</template>
<script>
import {ConnectedOverlayScrollHandler,DomHandler,ZIndexUtils} from 'primevue/utils';
import OverlayEventBus from 'primevue/overlayeventbus';
import Portal from 'primevue/portal';
export default {
name: 'ColorPicker',
emits: ['update:modelValue', 'change', 'show', 'hide'],
props: {
modelValue: {
type: null,
default: null
},
defaultColor: {
type: null,
default: 'ff0000'
},
inline: {
type: Boolean,
default: false
},
format: {
type: String,
default: 'hex'
},
disabled: {
type: Boolean,
default: false
},
tabindex: {
type: String,
default: null
},
autoZIndex: {
type: Boolean,
default: true
},
baseZIndex: {
type: Number,
default: 0
},
appendTo: {
type: String,
default: 'body'
},
panelClass: null
},
data() {
return {
overlayVisible: false
};
},
hsbValue: null,
outsideClickListener: null,
documentMouseMoveListener: null,
documentMouseUpListener: null,
scrollHandler: null,
resizeListener: null,
hueDragging: null,
colorDragging: null,
selfUpdate: null,
picker: null,
colorSelector: null,
colorHandle: null,
hueView: null,
hueHandle: null,
beforeUnmount() {
this.unbindOutsideClickListener();
this.unbindDragListeners();
this.unbindResizeListener();
if (this.scrollHandler) {
this.scrollHandler.destroy();
this.scrollHandler = null;
}
if (this.picker && this.autoZIndex) {
ZIndexUtils.clear(this.picker);
}
this.clearRefs();
},
mounted() {
this.updateUI();
},
watch: {
modelValue: {
immediate: true,
handler(newValue) {
this.hsbValue = this.toHSB(newValue);
if (this.selfUpdate)
this.selfUpdate = false;
else
this.updateUI();
}
}
},
methods: {
pickColor(event) {
let rect = this.colorSelector.getBoundingClientRect();
let top = rect.top + (window.pageYOffset || document.documentElement.scrollTop || document.body.scrollTop || 0);
let left = rect.left + document.body.scrollLeft;
let saturation = Math.floor(100 * (Math.max(0, Math.min(150, ((event.pageX || event.changedTouches[0].pageX)- left)))) / 150);
let brightness = Math.floor(100 * (150 - Math.max(0, Math.min(150, ((event.pageY || event.changedTouches[0].pageY) - top)))) / 150);
this.hsbValue = this.validateHSB({
h: this.hsbValue.h,
s: saturation,
b: brightness
});
this.selfUpdate = true;
this.updateColorHandle();
this.updateInput();
this.updateModel();
this.$emit('change', {event: event, value: this.modelValue});
},
pickHue(event) {
let top = this.hueView.getBoundingClientRect().top + (window.pageYOffset || document.documentElement.scrollTop || document.body.scrollTop || 0);
this.hsbValue = this.validateHSB({
h: Math.floor(360 * (150 - Math.max(0, Math.min(150, ((event.pageY || event.changedTouches[0].pageY) - top)))) / 150),
s: 100,
b: 100
});
this.selfUpdate = true;
this.updateColorSelector();
this.updateHue();
this.updateModel();
this.updateInput();
this.$emit('change', {event: event, value: this.modelValue});
},
updateModel() {
switch(this.format) {
case 'hex':
this.$emit('update:modelValue', this.HSBtoHEX(this.hsbValue));
break;
case 'rgb':
this.$emit('update:modelValue', this.HSBtoRGB(this.hsbValue));
break;
case 'hsb':
this.$emit('update:modelValue', this.hsbValue);
break;
default:
//NoOp
break;
}
},
updateColorSelector() {
if (this.colorSelector) {
let hsbValue = this.validateHSB({
h: this.hsbValue.h,
s: 100,
b: 100
});
this.colorSelector.style.backgroundColor = '#' + this.HSBtoHEX(hsbValue);
}
},
updateColorHandle() {
if (this.colorHandle) {
this.colorHandle.style.left = Math.floor(150 * this.hsbValue.s / 100) + 'px';
this.colorHandle.style.top = Math.floor(150 * (100 - this.hsbValue.b) / 100) + 'px';
}
},
updateHue() {
if (this.hueHandle) {
this.hueHandle.style.top = Math.floor(150 - (150 * this.hsbValue.h / 360)) + 'px';
}
},
updateInput() {
if (this.$refs.input) {
this.$refs.input.style.backgroundColor = '#' + this.HSBtoHEX(this.hsbValue);
}
},
updateUI() {
this.updateHue();
this.updateColorHandle();
this.updateInput();
this.updateColorSelector();
},
validateHSB(hsb) {
return {
h: Math.min(360, Math.max(0, hsb.h)),
s: Math.min(100, Math.max(0, hsb.s)),
b: Math.min(100, Math.max(0, hsb.b))
};
},
validateRGB(rgb) {
return {
r: Math.min(255, Math.max(0, rgb.r)),
g: Math.min(255, Math.max(0, rgb.g)),
b: Math.min(255, Math.max(0, rgb.b))
};
},
validateHEX(hex) {
var len = 6 - hex.length;
if (len > 0) {
var o = [];
for (var i=0; i<len; i++) {
o.push('0');
}
o.push(hex);
hex = o.join('');
}
return hex;
},
HEXtoRGB(hex) {
let hexValue = parseInt(((hex.indexOf('#') > -1) ? hex.substring(1) : hex), 16);
return {r: hexValue >> 16, g: (hexValue & 0x00FF00) >> 8, b: (hexValue & 0x0000FF)};
},
HEXtoHSB(hex) {
return this.RGBtoHSB(this.HEXtoRGB(hex));
},
RGBtoHSB(rgb) {
var hsb = {
h: 0,
s: 0,
b: 0
};
var min = Math.min(rgb.r, rgb.g, rgb.b);
var max = Math.max(rgb.r, rgb.g, rgb.b);
var delta = max - min;
hsb.b = max;
hsb.s = max !== 0 ? 255 * delta / max : 0;
if (hsb.s !== 0) {
if (rgb.r === max) {
hsb.h = (rgb.g - rgb.b) / delta;
} else if (rgb.g === max) {
hsb.h = 2 + (rgb.b - rgb.r) / delta;
} else {
hsb.h = 4 + (rgb.r - rgb.g) / delta;
}
} else {
hsb.h = -1;
}
hsb.h *= 60;
if (hsb.h < 0) {
hsb.h += 360;
}
hsb.s *= 100/255;
hsb.b *= 100/255;
return hsb;
},
HSBtoRGB(hsb) {
var rgb = {
r: null, g: null, b: null
};
var h = Math.round(hsb.h);
var s = Math.round(hsb.s*255/100);
var v = Math.round(hsb.b*255/100);
if (s === 0) {
rgb = {
r: v,
g: v,
b: v
}
}
else {
var t1 = v;
var t2 = (255-s)*v/255;
var t3 = (t1-t2)*(h%60)/60;
if (h===360) h = 0;
if (h<60) {rgb.r=t1; rgb.b=t2; rgb.g=t2+t3}
else if (h<120) {rgb.g=t1; rgb.b=t2; rgb.r=t1-t3}
else if (h<180) {rgb.g=t1; rgb.r=t2; rgb.b=t2+t3}
else if (h<240) {rgb.b=t1; rgb.r=t2; rgb.g=t1-t3}
else if (h<300) {rgb.b=t1; rgb.g=t2; rgb.r=t2+t3}
else if (h<360) {rgb.r=t1; rgb.g=t2; rgb.b=t1-t3}
else {rgb.r=0; rgb.g=0; rgb.b=0}
}
return {r:Math.round(rgb.r), g:Math.round(rgb.g), b:Math.round(rgb.b)};
},
RGBtoHEX(rgb) {
var hex = [
rgb.r.toString(16),
rgb.g.toString(16),
rgb.b.toString(16)
];
for (var key in hex) {
if (hex[key].length === 1) {
hex[key] = '0' + hex[key];
}
}
return hex.join('');
},
HSBtoHEX(hsb) {
return this.RGBtoHEX(this.HSBtoRGB(hsb));
},
toHSB(value) {
let hsb;
if (value) {
switch (this.format) {
case 'hex':
hsb = this.HEXtoHSB(value);
break;
case 'rgb':
hsb = this.RGBtoHSB(value);
break;
case 'hsb':
hsb = value;
break;
default:
break;
}
}
else {
hsb = this.HEXtoHSB(this.defaultColor);
}
return hsb;
},
onOverlayEnter(el) {
this.updateUI();
this.alignOverlay();
this.bindOutsideClickListener();
this.bindScrollListener();
this.bindResizeListener();
if (this.autoZIndex) {
ZIndexUtils.set('overlay', el, this.$primevue.config.zIndex.overlay);
}
this.$emit('show');
},
onOverlayLeave() {
this.unbindOutsideClickListener();
this.unbindScrollListener();
this.unbindResizeListener();
this.clearRefs();
this.$emit('hide');
},
onOverlayAfterLeave(el) {
if (this.autoZIndex) {
ZIndexUtils.clear(el);
}
},
alignOverlay() {
if (this.appendTo === 'self')
DomHandler.relativePosition(this.picker, this.$refs.input);
else
DomHandler.absolutePosition(this.picker, this.$refs.input);
},
onInputClick() {
if (this.disabled) {
return;
}
this.overlayVisible = !this.overlayVisible;
},
onInputKeydown(event) {
switch(event.which) {
//space
case 32:
this.overlayVisible = !this.overlayVisible;
event.preventDefault();
break;
//escape and tab
case 27:
case 9:
this.overlayVisible = false;
break;
default:
//NoOp
break;
}
},
onColorMousedown(event) {
if (this.disabled) {
return;
}
this.bindDragListeners();
this.onColorDragStart(event);
},
onColorDragStart(event) {
if (this.disabled) {
return;
}
this.colorDragging = true;
this.pickColor(event);
DomHandler.addClass(this.$el, 'p-colorpicker-dragging');
event.preventDefault();
},
onDrag(event) {
if (this.colorDragging) {
this.pickColor(event);
event.preventDefault();
}
if (this.hueDragging) {
this.pickHue(event);
event.preventDefault();
}
},
onDragEnd() {
this.colorDragging = false;
this.hueDragging = false;
DomHandler.removeClass(this.$el, 'p-colorpicker-dragging');
this.unbindDragListeners();
},
onHueMousedown(event) {
if (this.disabled) {
return;
}
this.bindDragListeners();
this.onHueDragStart(event);
},
onHueDragStart(event) {
if (this.disabled) {
return;
}
this.hueDragging = true;
this.pickHue(event);
DomHandler.addClass(this.$el, 'p-colorpicker-dragging');
},
isInputClicked(event) {
return this.$refs.input && this.$refs.input.isSameNode(event.target);
},
bindDragListeners() {
this.bindDocumentMouseMoveListener();
this.bindDocumentMouseUpListener();
},
unbindDragListeners() {
this.unbindDocumentMouseMoveListener();
this.unbindDocumentMouseUpListener();
},
bindOutsideClickListener() {
if (!this.outsideClickListener) {
this.outsideClickListener = (event) => {
if (this.overlayVisible && this.picker && !this.picker.contains(event.target) && !this.isInputClicked(event)) {
this.overlayVisible = false;
}
};
document.addEventListener('click', this.outsideClickListener);
}
},
unbindOutsideClickListener() {
if (this.outsideClickListener) {
document.removeEventListener('click', this.outsideClickListener);
this.outsideClickListener = null;
}
},
bindScrollListener() {
if (!this.scrollHandler) {
this.scrollHandler = new ConnectedOverlayScrollHandler(this.$refs.container, () => {
if (this.overlayVisible) {
this.overlayVisible = false;
}
});
}
this.scrollHandler.bindScrollListener();
},
unbindScrollListener() {
if (this.scrollHandler) {
this.scrollHandler.unbindScrollListener();
}
},
bindResizeListener() {
if (!this.resizeListener) {
this.resizeListener = () => {
if (this.overlayVisible && !DomHandler.isTouchDevice()) {
this.overlayVisible = false;
}
};
window.addEventListener('resize', this.resizeListener);
}
},
unbindResizeListener() {
if (this.resizeListener) {
window.removeEventListener('resize', this.resizeListener);
this.resizeListener = null;
}
},
bindDocumentMouseMoveListener() {
if (!this.documentMouseMoveListener) {
this.documentMouseMoveListener = this.onDrag.bind(this);
document.addEventListener('mousemove', this.documentMouseMoveListener);
}
},
unbindDocumentMouseMoveListener() {
if (this.documentMouseMoveListener) {
document.removeEventListener('mousemove', this.documentMouseMoveListener);
this.documentMouseMoveListener = null;
}
},
bindDocumentMouseUpListener() {
if (!this.documentMouseUpListener) {
this.documentMouseUpListener = this.onDragEnd.bind(this);
document.addEventListener('mouseup', this.documentMouseUpListener);
}
},
unbindDocumentMouseUpListener() {
if (this.documentMouseUpListener) {
document.removeEventListener('mouseup', this.documentMouseUpListener);
this.documentMouseUpListener = null;
}
},
pickerRef(el) {
this.picker = el
},
colorSelectorRef(el) {
this.colorSelector = el;
},
colorHandleRef(el) {
this.colorHandle = el;
},
hueViewRef(el) {
this.hueView = el;
},
hueHandleRef(el) {
this.hueHandle = el;
},
clearRefs() {
this.picker = null;
this.colorSelector = null;
this.colorHandle = null;
this.hueView = null;
this.hueHandle = null;
},
onOverlayClick(event) {
OverlayEventBus.emit('overlay-click', {
originalEvent: event,
target: this.$el
});
}
},
computed: {
containerClass() {
return ['p-colorpicker p-component', {'p-colorpicker-overlay': !this.inline}];
},
inputClass() {
return ['p-colorpicker-preview p-inputtext', {'p-disabled': this.disabled}];
},
pickerClass() {
return ['p-colorpicker-panel', this.panelClass, {
'p-colorpicker-overlay-panel': !this.inline, 'p-disabled': this.disabled,
'p-input-filled': this.$primevue.config.inputStyle === 'filled',
'p-ripple-disabled': this.$primevue.config.ripple === false
}];
}
},
components: {
'Portal': Portal
}
}
</script>
<style>
.p-colorpicker {
display: inline-block;
}
.p-colorpicker-dragging {
cursor: pointer;
}
.p-colorpicker-overlay {
position: relative;
}
.p-colorpicker-panel {
position: relative;
width: 193px;
height: 166px;
}
.p-colorpicker-overlay-panel {
position: absolute;
top: 0;
left: 0;
}
.p-colorpicker-preview {
cursor: pointer;
}
.p-colorpicker-panel .p-colorpicker-content {
position: relative;
}
.p-colorpicker-panel .p-colorpicker-color-selector {
width: 150px;
height: 150px;
top: 8px;
left: 8px;
position: absolute;
}
.p-colorpicker-panel .p-colorpicker-color {
width: 150px;
height: 150px;
}
.p-colorpicker-panel .p-colorpicker-color-handle {
position: absolute;
top: 0px;
left: 150px;
border-radius: 100%;
width: 10px;
height: 10px;
border-width: 1px;
border-style: solid;
margin: -5px 0 0 -5px;
cursor: pointer;
opacity: .85;
}
.p-colorpicker-panel .p-colorpicker-hue {
width: 17px;
height: 150px;
top: 8px;
left: 167px;
position: absolute;
opacity: .85;
}
.p-colorpicker-panel .p-colorpicker-hue-handle {
position: absolute;
top: 150px;
left: 0px;
width: 21px;
margin-left: -2px;
margin-top: -5px;
height: 10px;
border-width: 2px;
border-style: solid;
opacity: .85;
cursor: pointer;
}
</style>

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 293 B

View File

@ -0,0 +1,9 @@
{
"main": "./colorpicker.cjs.js",
"module": "./colorpicker.esm.js",
"unpkg": "./colorpicker.min.js",
"types": "./ColorPicker.d.ts",
"browser": {
"./sfc": "./ColorPicker.vue"
}
}

488
components/column/Column.d.ts vendored Executable file
View File

@ -0,0 +1,488 @@
import { VNode } from 'vue';
import { ClassComponent, GlobalComponentConstructor } from '../ts-helpers';
import { VirtualScrollerLoaderOptions } from '../virtualscroller';
type ColumnFieldType = string | ((item: any) => string) | undefined;
type ColumnSelectionModeType = 'single' | 'multiple' | undefined;
type ColumnAlignFrozenType = 'left' | 'right' | undefined;
export interface ColumnFilterModelType {
/**
* Value of filterModel.
*/
value: any;
/**
* Match mode of filterModel.
*/
matchMode: string;
}
export interface ColumnFilterMatchModeOptions {
[key: string]: string;
}
export interface ColumnLoadingOptions extends VirtualScrollerLoaderOptions {
/**
* Cell index
*/
cellIndex: number;
/**
* Whether the cell is first.
*/
cellFirst: boolean;
/**
* Whether the cell is last.
*/
cellLast: boolean;
/**
* Whether the cell is even.
*/
cellEven: boolean;
/**
* Whether the item is odd.
*/
cellOdd: boolean;
/**
* Column instance
*/
column: Column;
/**
* Column field
*/
field: string;
}
export interface ColumnProps {
/**
* Identifier of a column if field property is not defined.
*/
columnKey?: string | undefined;
/**
* Property represented by the column.
* @see ColumnFieldType
*/
field?: ColumnFieldType;
/**
* Property name to use in sorting, defaults to field.
* @see ColumnFieldType
*/
sortField?: ColumnFieldType;
/**
* Property name to use in filtering, defaults to field.
* @see ColumnFieldType
*/
filterField?: ColumnFieldType;
/**
* Type of data. It's value is related to PrimeVue.filterMatchModeOptions config.
*/
dataType?: string | undefined;
/**
* Defines if a column is sortable.
*/
sortable?: boolean | undefined;
/**
* Header content of the column.
*/
header?: string | undefined;
/**
* Footer content of the column.
*/
footer?: string | undefined;
/**
* Inline style of header, body and footer cells.
*/
style?: any;
/**
* Style class of header, body and footer cells.
*/
class?: any;
/**
* Inline style of the column header.
*/
headerStyle?: any;
/**
* Style class of the column header.
*/
headerClass?: any;
/**
* Inline style of the column body.
*/
bodyStyle?: any;
/**
* Style class of the column body.
*/
bodyClass?: any;
/**
* Inline style of the column footer.
*/
footerStyle?: any;
/**
* Style class of the column footer.
*/
footerClass?: any;
/**
* Whether to display the filter overlay.
* Default value is true.
*/
showFilterMenu?: boolean | undefined;
/**
* When enabled, match all and match any operator selector is displayed.
* Default value is true.
*/
showFilterOperator?: boolean | undefined;
/**
* Displays a button to clear the column filtering.
* Default value is true.
*/
showClearButton?: boolean | undefined;
/**
* Displays a button to apply the column filtering.
* Default value is true.
*/
showApplyButton?: boolean | undefined;
/**
* Whether to show the match modes selector.
* Default value is true.
*/
showFilterMatchModes?: boolean | undefined;
/**
* When enabled, a button is displayed to add more rules.
* Default value is true.
*/
showAddButton?: boolean | undefined;
/**
* An array of label-value pairs to override the global match mode options.
*/
filterMatchModeOptions?: ColumnFilterMatchModeOptions[];
/**
* Maximum number of constraints for a column filter.
* Default value is 2.
*/
maxConstraints?: number | undefined;
/**
* Whether to exclude from global filtering or not.
*/
excludeGlobalFilter?: boolean | undefined;
/**
* Inline style of the column filter header in row filter display.
*/
filterHeaderStyle?: any;
/**
* Style class of the column filter header in row filter display.
*/
filterHeaderClass?: any;
/**
* Inline style of the column filter overlay.
*/
filterMenuStyle?: any;
/**
* Style class of the column filter overlay.
*/
filterMenuClass?: any;
/**
* Defines column based selection mode, options are 'single' and 'multiple'.
* @see ColumnSelectionModeType
*/
selectionMode?: ColumnSelectionModeType;
/**
* Displays an icon to toggle row expansion.
*/
expander?: boolean | undefined;
/**
* Number of columns to span for grouping.
*/
colspan?: number | undefined;
/**
* Number of rows to span for grouping.
*/
rowspan?: number | undefined;
/**
* Whether this column displays an icon to reorder the rows.
*/
rowReorder?: boolean | undefined;
/**
* Icon of the drag handle to reorder rows.
* Default value is 'pi pi-bars'.
*/
rowReorderIcon?: string | undefined;
/**
* Defines if the column itself can be reordered with dragging.
*/
reorderableColumn?: boolean | undefined;
/**
* When enabled, column displays row editor controls.
*/
rowEditor?: boolean | undefined;
/**
* Whether the column is fixed in horizontal scrolling.
*/
frozen?: boolean | undefined;
/**
* Position of a frozen column, valid values are left and right.
* @see ColumnAlignFrozenType
* Default value is 'left'.
*/
alignFrozen?: ColumnAlignFrozenType;
/**
* Whether the column is included in data export.
*/
exportable?: boolean | undefined;
/**
* Custom export header of the column to be exported as CSV.
*/
exportHeader?: string | undefined;
/**
* Custom export footer of the column to be exported as CSV.
*/
exportFooter?: string | undefined;
/**
* Defines the filtering algorithm to use when searching the options.
*/
filterMatchMode?: string | undefined;
/**
* Whether the column is rendered.
*/
hidden?: boolean | undefined;
}
export interface ColumnSlots {
/**
* Custom body template.
* @param {Object} scope - body slot's params.
*/
body: (scope: {
/**
* Row data.
*/
data: any;
/**
* Row node data.
*/
node: any;
/**
* Column node.
*/
column: Column;
/**
* Column field.
*/
field: string;
/**
* Row index.
*/
index: number;
/**
* Whether the row is frozen.
*/
frozenRow: boolean;
/**
* Callback function
*/
editorInitCallback: () => void;
}) => VNode[];
/**
* Custom header template.
* @param {Object} scope - header slot's params.
*/
header: (scope: {
/**
* Column node.
*/
column: Column;
}) => VNode[];
/**
* Custom footer template.
* @param {Object} scope - footer slot's params.
*/
footer: (scope: {
/**
* Column node.
*/
column: Column;
}) => VNode[];
/**
* Custom editor template.
* @param {Object} scope - editor slot's params.
*/
editor: (scope: {
/**
* Row data.
*/
data: any;
/**
* Column node.
*/
column: Column;
/**
* Column field.
*/
field: string;
/**
* Row index.
*/
index: number;
/**
* Whether the row is frozen.
*/
frozenRow: boolean;
/**
* Callback function
*/
editorSaveCallback: () => void;
/**
* Callback function
*/
editorCancelCallback: () => void;
}) => VNode[];
/**
* Custom filter template.
* @param {Object} scope - filter slot's params.
*/
filter: (scope: {
/**
* Column field.
*/
field: string;
/**
* Filter metadata
* @see ColumnFilterModelType
*/
filterModel: ColumnFilterModelType;
/**
* Callback function
*/
filterCallback: () => void;
}) => VNode[];
/**
* Custom filter header template.
* @param {Object} scope - filter header slot's params.
*/
filterheader: (scope: {
/**
* Column field.
*/
field: string;
/**
* Filter metadata
* @see ColumnFilterModelType
*/
filterModel: ColumnFilterModelType;
/**
* Callback function
*/
filterCallback: () => void;
}) => VNode[];
/**
* Custom filter footer template.
* @param {Object} scope - filter footer slot's params.
*/
filterfooter: (scope: {
/**
* Column field.
*/
field: string;
/**
* Filter metadata
* @see ColumnFilterModelType
*/
filterModel: ColumnFilterModelType;
/**
* Callback function
*/
filterCallback: () => void;
}) => VNode[];
/**
* Custom filter clear template.
* @param {Object} scope - filter clear slot's params.
*/
filterclear: (scope: {
/**
* Column field.
*/
field: string;
/**
* Filter metadata
* @see ColumnFilterModelType
*/
filterModel: ColumnFilterModelType;
/**
* Callback function
*/
filterCallback: () => void;
}) => VNode[];
/**
* Custom filter apply template.
* @param {Object} scope - filter apply slot's params.
*/
filterapply: (scope: {
/**
* Column field.
*/
field: string;
/**
* Filter metadata
* @see ColumnFilterModelType
*/
filterModel: ColumnFilterModelType;
/**
* Callback function
*/
filterCallback: () => void;
}) => VNode[];
/**
* Custom loading template.
* @param {Object} scope - loading slot's params.
*/
loading: (scope: {
/**
* Row data.
*/
data: any;
/**
* Column node.
*/
column: Column;
/**
* Column field.
*/
field: string;
/**
* Row index.
*/
index: number;
/**
* Whether the row is frozen.
*/
frozenRow: boolean;
/**
* Loading options.
* @see ColumnLoadingOptions
*/
loadingOptions: ColumnLoadingOptions;
}) => VNode[];
}
export declare type ColumnEmits = {
}
declare class Column extends ClassComponent<ColumnProps, ColumnSlots, ColumnEmits> { }
declare module '@vue/runtime-core' {
interface GlobalComponents {
Column: GlobalComponentConstructor<Column>
}
}
/**
*
* Column is a helper component for DataTable and TreeTable.
*
* Demos:
*
* - [DataTable](https://www.primefaces.org/primevue/showcase/#/datatable)
* - [TreeTable](https://www.primefaces.org/primevue/showcase/#/treetable)
*
*/
export default Column;

186
components/column/Column.vue Executable file
View File

@ -0,0 +1,186 @@
<script>
export default {
name: 'Column',
props: {
columnKey: {
type: null,
default: null
},
field: {
type: [String, Function],
default: null
},
sortField: {
type: [String, Function],
default: null
},
filterField: {
type: [String, Function],
default: null
},
dataType: {
type: String,
default: 'text'
},
sortable: {
type: Boolean,
default: false
},
header: {
type: null,
default: null
},
footer: {
type: null,
default: null
},
style: {
type: null,
default: null
},
class: {
type: String,
default: null
},
headerStyle: {
type: null,
default: null
},
headerClass: {
type: String,
default: null
},
bodyStyle: {
type: null,
default: null
},
bodyClass: {
type: String,
default: null
},
footerStyle: {
type: null,
default: null
},
footerClass: {
type: String,
default: null
},
showFilterMenu: {
type: Boolean,
default: true
},
showFilterOperator: {
type: Boolean,
default: true
},
showClearButton: {
type: Boolean,
default: true
},
showApplyButton: {
type: Boolean,
default: true
},
showFilterMatchModes: {
type: Boolean,
default: true
},
showAddButton: {
type: Boolean,
default: true
},
filterMatchModeOptions: {
type: Array,
default: null
},
maxConstraints: {
type: Number,
default: 2
},
excludeGlobalFilter: {
type: Boolean,
default: false
},
filterHeaderClass: {
type: String,
default: null
},
filterHeaderStyle: {
type: null,
default: null
},
filterMenuClass: {
type: String,
default: null
},
filterMenuStyle: {
type: null,
default: null
},
selectionMode: {
type: String,
default: null
},
expander: {
type: Boolean,
default: false
},
colspan: {
type: Number,
default: null
},
rowspan: {
type: Number,
default: null
},
rowReorder: {
type: Boolean,
default: false
},
rowReorderIcon: {
type: String,
default: 'pi pi-bars'
},
reorderableColumn: {
type: Boolean,
default: true
},
rowEditor: {
type: Boolean,
default: false
},
frozen: {
type: Boolean,
default: false
},
alignFrozen: {
type: String,
default: 'left'
},
exportable: {
type: Boolean,
default: true
},
exportHeader: {
type: String,
default: null
},
exportFooter: {
type: String,
default: null
},
filterMatchMode: {
type: String,
default: null
},
hidden: {
type: Boolean,
default: false
}
},
render() {
return null;
}
}
</script>

View File

@ -0,0 +1,9 @@
{
"main": "./column.cjs.js",
"module": "./column.esm.js",
"unpkg": "./column.min.js",
"types": "./Column.d.ts",
"browser": {
"./sfc": "./Column.vue"
}
}

35
components/columngroup/ColumnGroup.d.ts vendored Executable file
View File

@ -0,0 +1,35 @@
import { ClassComponent, GlobalComponentConstructor } from '../ts-helpers';
type ColumnGroupType = 'header' | 'footer' | undefined;
export interface ColumnGroupProps {
/**
* Type of column group
*/
type?: ColumnGroupType;
}
export interface ColumnGroupSlots {
}
export declare type ColumnGroupEmits = {
}
declare class ColumnGroup extends ClassComponent<ColumnGroupProps, ColumnGroupSlots, ColumnGroupEmits> { }
declare module '@vue/runtime-core' {
interface GlobalComponents {
ColumnGroup: GlobalComponentConstructor<ColumnGroup>
}
}
/**
*
* ColumnGroup is a helper component to create grouped header and footer on DataTable.
*
* Demos:
*
* - [ColumnGroup](https://www.primefaces.org/primevue/showcase/#/datatable/colgroup)
*
*/
export default ColumnGroup;

View File

@ -0,0 +1,14 @@
<script>
export default {
name: 'ColumnGroup',
props: {
type: {
type: String,
default: null
}
},
render() {
return null;
}
}
</script>

View File

@ -0,0 +1,9 @@
{
"main": "./columngroup.cjs.js",
"module": "./columngroup.esm.js",
"unpkg": "./columngroup.min.js",
"types": "./ColumnGroup.d.ts",
"browser": {
"./sfc": "./ColumnGroup.vue"
}
}

168
components/common/Common.css Executable file
View File

@ -0,0 +1,168 @@
.p-component, .p-component * {
box-sizing: border-box;
}
.p-hidden {
display: none;
}
.p-hidden-space {
visibility: hidden;
}
.p-hidden-accessible {
border: 0;
clip: rect(0 0 0 0);
height: 1px;
margin: -1px;
overflow: hidden;
padding: 0;
position: absolute;
width: 1px;
}
.p-hidden-accessible input,
.p-hidden-accessible select {
transform: scale(0);
}
.p-reset {
margin: 0;
padding: 0;
border: 0;
outline: 0;
text-decoration: none;
font-size: 100%;
list-style: none;
}
.p-disabled, .p-disabled * {
cursor: default !important;
pointer-events: none;
user-select: none;
}
.p-component-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
.p-overflow-hidden {
overflow: hidden;
}
.p-unselectable-text {
user-select: none;
}
.p-scrollbar-measure {
width: 100px;
height: 100px;
overflow: scroll;
position: absolute;
top: -9999px;
}
@-webkit-keyframes p-fadein {
0% { opacity: 0; }
100% { opacity: 1; }
}
@keyframes p-fadein {
0% { opacity: 0; }
100% { opacity: 1; }
}
input[type="button"],
input[type="submit"],
input[type="reset"],
input[type="file"]::-webkit-file-upload-button,
button {
border-radius: 0;
}
.p-link {
text-align: left;
background-color: transparent;
margin: 0;
padding: 0;
border: none;
cursor: pointer;
user-select: none;
}
.p-link:disabled {
cursor: default;
}
/* Non vue overlay animations */
.p-connected-overlay {
opacity: 0;
transform: scaleY(0.8);
transition: transform .12s cubic-bezier(0, 0, 0.2, 1), opacity .12s cubic-bezier(0, 0, 0.2, 1);
}
.p-connected-overlay-visible {
opacity: 1;
transform: scaleY(1);
}
.p-connected-overlay-hidden {
opacity: 0;
transform: scaleY(1);
transition: opacity .1s linear;
}
/* Vue based overlay animations */
.p-connected-overlay-enter-from {
opacity: 0;
transform: scaleY(0.8);
}
.p-connected-overlay-leave-to {
opacity: 0;
}
.p-connected-overlay-enter-active {
transition: transform .12s cubic-bezier(0, 0, 0.2, 1), opacity .12s cubic-bezier(0, 0, 0.2, 1);
}
.p-connected-overlay-leave-active {
transition: opacity .1s linear;
}
/* Toggleable Content */
.p-toggleable-content-enter-from,
.p-toggleable-content-leave-to {
max-height: 0;
}
.p-toggleable-content-enter-to,
.p-toggleable-content-leave-from {
max-height: 1000px;
}
.p-toggleable-content-leave-active {
overflow: hidden;
transition: max-height 0.45s cubic-bezier(0, 1, 0, 1);
}
.p-toggleable-content-enter-active {
overflow: hidden;
transition: max-height 1s ease-in-out;
}
.p-sr-only {
border: 0;
clip: rect(1px, 1px, 1px, 1px);
clip-path: inset(50%);
height: 1px;
margin: -1px;
overflow: hidden;
padding: 0;
position: absolute;
width: 1px;
word-wrap: normal !important;
}

View File

@ -0,0 +1,3 @@
import {EventBus} from 'primevue/utils';
export default EventBus();

View File

@ -0,0 +1,5 @@
{
"main": "./confirmationeventbus.cjs.js",
"module": "./confirmationeventbus.esm.js",
"unpkg": "./confirmationeventbus.min.js"
}

View File

@ -0,0 +1,70 @@
type ConfirmationPositionType = 'center' | 'top' | 'bottom' | 'left' | 'right' | 'topleft' | 'topright' | 'bottomleft' | 'bottomright' | undefined;
export interface ConfirmationOptions {
/**
* Element to align the overlay.
*/
target?: HTMLElement | undefined;
/**
* Header text of the dialog.
*/
header?: string | undefined;
/**
* Message of the confirmation.
*/
message?: string | undefined;
/**
* Optional key to match the key of the confirmation, useful to target a specific confirm dialog instance.
*/
group?: string | undefined;
/**
* Position of the dialog, options are 'center', 'top', 'bottom', 'left', 'right', 'topleft', 'topright', 'bottomleft' or 'bottomright'.
* @see ConfirmationPositionType
* Default value is 'center'.
*/
position?: ConfirmationPositionType;
/**
* Icon to display next to the message.
*/
icon?: string | undefined;
/**
* Whether background scroll should be blocked when dialog is visible.
*/
blockScroll?: boolean | undefined;
/**
* Callback to execute when action is confirmed.
*/
accept?: () => void;
/**
* Callback to execute when action is rejected.
*/
reject?: () => void;
/**
* Label of the accept button. Defaults to PrimeVue Locale configuration.
*/
acceptLabel?: string | undefined;
/**
* Label of the reject button. Defaults to PrimeVue Locale configuration.
*/
rejectLabel?: string | undefined;
/**
* Icon of the accept button.
*/
acceptIcon?: string | undefined;
/**
* Icon of the reject button.
*/
rejectIcon?: string | undefined;
/**
* Style class of the accept button.
*/
acceptClass?: string | undefined;
/**
* Style class of the reject button.
*/
rejectClass?: string | undefined;
/**
* Element to receive the focus when the dialog gets visible, valid values are "accept" and "reject".
*/
defaultFocus?: string | undefined;
}

View File

@ -0,0 +1,3 @@
{
"types": "./ConfirmationOptions.d.ts"
}

View File

@ -0,0 +1,29 @@
import Vue, { Plugin } from 'vue';
import { ConfirmationOptions } from '../confirmationoptions';
declare const plugin: Plugin;
export default plugin;
export interface ConfirmationServiceMethods {
/**
* Displays the dialog using the confirmation object options.
* @param {ConfirmationOptions} options - Confirmation Object
*/
require(options: ConfirmationOptions): void;
/**
* Hides the dialog without invoking accept or reject callbacks.
*/
close(): void;
}
declare module 'vue/types/vue' {
interface Vue {
$confirm: ConfirmationServiceMethods;
}
}
declare module '@vue/runtime-core' {
interface ComponentCustomProperties {
$confirm: ConfirmationServiceMethods;
}
}

View File

@ -0,0 +1,17 @@
import ConfirmationEventBus from 'primevue/confirmationeventbus';
import {PrimeVueConfirmSymbol} from 'primevue/useconfirm';
export default {
install: (app) => {
const ConfirmationService = {
require: (options) => {
ConfirmationEventBus.emit('confirm', options);
},
close: () => {
ConfirmationEventBus.emit('close');
}
};
app.config.globalProperties.$confirm = ConfirmationService;
app.provide(PrimeVueConfirmSymbol, ConfirmationService);
}
};

View File

@ -0,0 +1,6 @@
{
"main": "./confirmationservice.cjs.js",
"module": "./confirmationservice.esm.js",
"unpkg": "./confirmationservice.min.js",
"types": "./ConfirmationService.d.ts"
}

View File

@ -0,0 +1,71 @@
import { VNode } from 'vue';
import { ClassComponent, GlobalComponentConstructor } from '../ts-helpers';
import { ConfirmationOptions } from '../confirmationoptions';
export interface ConfirmDialogBreakpoints {
/**
* Breakpoint for responsive mode.
*
* Example:
*
* <ConfirmDialog :breakpoints="{'960px': '75vw', '640px': '100vw'}" ... />
*
* Result:
*
* @media screen and (max-width: ${breakpoint[key]}) {
* .p-dialog[attributeSelector] {
* width: ${breakpoint[value]} !important;
* }
* }
*/
[key: string]: string;
}
export interface ConfirmDialogProps {
/**
* Optional key to match the key of the confirmation, useful to target a specific confirm dialog instance.
*/
group?: string | undefined;
/**
* Object literal to define widths per screen size.
* @see ConfirmDialogBreakpoints
*/
breakpoints?: ConfirmDialogBreakpoints;
}
export interface ConfirmDialogSlots {
/**
* Custom message template.
* @param {Object} scope - message slot's params.
*/
message: (scope: {
message: ConfirmationOptions;
}) => VNode[];
}
export declare type ConfirmDialogEmits = {
}
declare class ConfirmDialog extends ClassComponent<ConfirmDialogProps, ConfirmDialogSlots, ConfirmDialogEmits> { }
declare module '@vue/runtime-core' {
interface GlobalComponents {
ConfirmDialog: GlobalComponentConstructor<ConfirmDialog>
}
}
/**
*
* ConfirmDialog uses a Dialog UI that is integrated with the Confirmation API.
*
* Helper API:
*
* - Confirmation API
* - ConfirmationService
*
* Demos:
*
* - [ConfirmDialog](https://www.primefaces.org/primevue/showcase/#/confirmdialog)
*
*/
export default ConfirmDialog;

View File

@ -0,0 +1,168 @@
import { mount } from '@vue/test-utils';
import PrimeVue from '@/components/config/PrimeVue';
import ConfirmDialog from './ConfirmDialog.vue';
describe('ConfirmDialog', () => {
it('should exist', async() => {
const wrapper = mount(ConfirmDialog, {
global: {
plugins: [PrimeVue],
stubs: {
teleport: true,
transition: false
},
},
data() {
return {
confirmation: {
message: 'Are you sure you want to proceed?',
header: 'Confirmation',
icon: 'pi pi-exclamation-triangle'
}
}
}
});
await wrapper.setData({ visible: true });
expect(wrapper.find('.p-dialog-mask .p-dialog.p-component').exists()).toBe(true);
expect(wrapper.find('.p-dialog-title').text()).toBe('Confirmation');
expect(wrapper.find('.p-confirm-dialog-message').text()).toBe('Are you sure you want to proceed?');
await wrapper.vm.reject();
expect(wrapper.find('.p-dialog-mask .p-dialog.p-component').exists()).toBe(false);
});
it('should dialog trigger the accept function', async() => {
const wrapper = mount(ConfirmDialog, {
global: {
plugins: [PrimeVue],
stubs: {
teleport: true,
transition: false
}
},
data() {
return {
confirmation: {
message: 'Are you sure you want to proceed?',
header: 'Confirmation',
icon: 'pi pi-exclamation-triangle',
accept: () => {
console.log('accept')
},
reject: () => {
console.log('reject');
}
}
}
}
});
const acceptTriggered = jest.spyOn(wrapper.componentVM.confirmation, 'accept');
await wrapper.setData({ visible: true });
const CDAcceptBtn = wrapper.find('.p-confirm-dialog-accept');
await CDAcceptBtn.trigger('click');
expect(acceptTriggered).toBeCalled();
});
it('should dialog trigger the reject function', async() => {
const wrapper = mount(ConfirmDialog, {
global: {
plugins: [PrimeVue],
stubs: {
teleport: true,
transition: false
}
},
data() {
return {
confirmation: {
message: 'Are you sure you want to proceed?',
header: 'Confirmation',
icon: 'pi pi-exclamation-triangle',
accept: () => {
console.log('accept')
},
reject: () => {
console.log('reject');
}
}
}
}
});
const rejectTriggered = jest.spyOn(wrapper.componentVM.confirmation, 'reject');
await wrapper.setData({ visible: true });
const CDRejectBtn = wrapper.find('.p-confirm-dialog-reject');
await CDRejectBtn.trigger('click');
expect(rejectTriggered).toBeCalled();
});
it('should dialog close button work', async() => {
const wrapper = mount(ConfirmDialog, {
global: {
plugins: [PrimeVue],
stubs: {
teleport: true,
transition: false
}
},
data() {
return {
confirmation: {
message: 'Are you sure you want to proceed?',
header: 'Confirmation',
icon: 'pi pi-exclamation-triangle'
}
}
}
});
await wrapper.setData({ visible: true });
const dialogCloseBtn = wrapper.find('.p-dialog-header-close');
await dialogCloseBtn.trigger('click');
expect(wrapper.find('.p-dialog-mask .p-dialog.p-component').exists()).toBe(false);
});
it('should position work', async () => {
const wrapper = mount(ConfirmDialog, {
global: {
plugins: [PrimeVue],
stubs: {
teleport: true,
transition: false
}
},
data() {
return {
confirmation: {
group: 'positionDialog',
message: 'Do you want to delete this record?',
header: 'Delete Confirmation',
icon: 'pi pi-info-circle',
position: 'bottom'
}
}
}
});
await wrapper.setData({ visible: true });
expect(wrapper.find('.p-dialog-mask.p-dialog-bottom').exists()).toBe(true);
});
});

View File

@ -0,0 +1,126 @@
<template>
<CDialog v-model:visible="visible" :modal="true" :header="header" :blockScroll="blockScroll" :position="position" class="p-confirm-dialog"
:breakpoints="breakpoints" :closeOnEscape="closeOnEscape">
<template v-if="!$slots.message">
<i :class="iconClass" />
<span class="p-confirm-dialog-message">{{ message }}</span>
</template>
<component v-else :is="$slots.message" :message="confirmation"></component>
<template #footer>
<CDButton :label="rejectLabel" :icon="rejectIcon" :class="rejectClass" @click="reject()" :autofocus="autoFocusReject"/>
<CDButton :label="acceptLabel" :icon="acceptIcon" :class="acceptClass" @click="accept()" :autofocus="autoFocusAccept" />
</template>
</CDialog>
</template>
<script>
import ConfirmationEventBus from 'primevue/confirmationeventbus';
import Dialog from 'primevue/dialog';
import Button from 'primevue/button';
export default {
name: 'ConfirmDialog',
props: {
group: String,
breakpoints: {
type: Object,
default: null
}
},
confirmListener: null,
closeListener: null,
data() {
return {
visible: false,
confirmation: null,
}
},
mounted() {
this.confirmListener = (options) => {
if (!options) {
return;
}
if (options.group === this.group) {
this.confirmation = options;
this.visible = true;
}
};
this.closeListener = () => {
this.visible = false;
this.confirmation = null;
};
ConfirmationEventBus.on('confirm', this.confirmListener);
ConfirmationEventBus.on('close', this.closeListener);
},
beforeUnmount() {
ConfirmationEventBus.off('confirm', this.confirmListener);
ConfirmationEventBus.off('close', this.closeListener);
},
methods: {
accept() {
if (this.confirmation.accept) {
this.confirmation.accept();
}
this.visible = false;
},
reject() {
if (this.confirmation.reject) {
this.confirmation.reject();
}
this.visible = false;
}
},
computed: {
header() {
return this.confirmation ? this.confirmation.header : null;
},
message() {
return this.confirmation ? this.confirmation.message : null;
},
blockScroll() {
return this.confirmation ? this.confirmation.blockScroll : true;
},
position() {
return this.confirmation ? this.confirmation.position : null;
},
iconClass() {
return ['p-confirm-dialog-icon', this.confirmation ? this.confirmation.icon : null];
},
acceptLabel() {
return this.confirmation ? (this.confirmation.acceptLabel || this.$primevue.config.locale.accept) : null;
},
rejectLabel() {
return this.confirmation ? (this.confirmation.rejectLabel || this.$primevue.config.locale.reject) : null;
},
acceptIcon() {
return this.confirmation ? this.confirmation.acceptIcon : null;
},
rejectIcon() {
return this.confirmation ? this.confirmation.rejectIcon : null;
},
acceptClass() {
return ['p-confirm-dialog-accept', this.confirmation ? this.confirmation.acceptClass : null];
},
rejectClass() {
return ['p-confirm-dialog-reject', this.confirmation ? (this.confirmation.rejectClass || 'p-button-text') : null];
},
autoFocusAccept() {
return (this.confirmation.defaultFocus === undefined || this.confirmation.defaultFocus === 'accept') ? true : false;
},
autoFocusReject() {
return this.confirmation.defaultFocus === 'reject' ? true : false;
},
closeOnEscape() {
return this.confirmation ? this.confirmation.closeOnEscape : true;
}
},
components: {
'CDialog': Dialog,
'CDButton': Button
}
}
</script>

Some files were not shown because too many files have changed in this diff Show More