Add Tom-Select lib

This commit is contained in:
jf-cbd
2025-11-13 10:48:06 +01:00
parent cef8fbc859
commit 7733f13d14
354 changed files with 53014 additions and 2 deletions

View File

@@ -0,0 +1,73 @@
/**
* Plugin: "dropdown_input" (Tom Select)
* Copyright (c) contributors
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this
* file except in compliance with the License. You may obtain a copy of the License at:
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under
* the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF
* ANY KIND, either express or implied. See the License for the specific language
* governing permissions and limitations under the License.
*
*/
import type TomSelect from '../../tom-select.ts';
import { nodeIndex, removeClasses } from '../../vanilla.ts';
export default function(this:TomSelect) {
var self = this;
/**
* Moves the caret to the specified index.
*
* The input must be moved by leaving it in place and moving the
* siblings, due to the fact that focus cannot be restored once lost
* on mobile webkit devices
*
*/
self.hook('instead','setCaret',(new_pos:number) => {
if( self.settings.mode === 'single' || !self.control.contains(self.control_input) ) {
new_pos = self.items.length;
} else {
new_pos = Math.max(0, Math.min(self.items.length, new_pos));
if( new_pos != self.caretPos && !self.isPending ){
self.controlChildren().forEach((child,j) => {
if( j < new_pos ){
self.control_input.insertAdjacentElement('beforebegin', child );
} else {
self.control.appendChild( child );
}
});
}
}
self.caretPos = new_pos;
});
self.hook('instead','moveCaret',(direction:number) => {
if( !self.isFocused ) return;
// move caret before or after selected items
const last_active = self.getLastActive(direction);
if( last_active ){
const idx = nodeIndex(last_active);
self.setCaret(direction > 0 ? idx + 1: idx);
self.setActiveItem();
removeClasses(last_active as HTMLElement,'last-active');
// move caret left or right of current position
}else{
self.setCaret(self.caretPos + direction);
}
});
};

View File

@@ -0,0 +1,23 @@
/**
* Plugin: "change_listener" (Tom Select)
* Copyright (c) contributors
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this
* file except in compliance with the License. You may obtain a copy of the License at:
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under
* the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF
* ANY KIND, either express or implied. See the License for the specific language
* governing permissions and limitations under the License.
*
*/
import type TomSelect from '../../tom-select.ts';
import { addEvent } from '../../utils.ts';
export default function(this:TomSelect) {
addEvent(this.input,'change',()=>{
this.sync();
});
};

View File

@@ -0,0 +1,11 @@
.plugin-checkbox_options:not(.rtl) {
.option input {
margin-right: 0.5rem;
}
}
.plugin-checkbox_options.rtl {
.option input {
margin-left: 0.5rem;
}
}

View File

@@ -0,0 +1,130 @@
/**
* Plugin: "checkbox_options" (Tom Select)
* Copyright (c) contributors
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this
* file except in compliance with the License. You may obtain a copy of the License at:
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under
* the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF
* ANY KIND, either express or implied. See the License for the specific language
* governing permissions and limitations under the License.
*
*/
import type TomSelect from '../../tom-select.ts';
import { TomTemplate } from '../../types/index.ts';
import { preventDefault, hash_key } from '../../utils.ts';
import { getDom } from '../../vanilla.ts';
import { CBOptions } from './types.ts';
export default function(this:TomSelect, userOptions:CBOptions) {
var self = this;
var orig_onOptionSelect = self.onOptionSelect;
self.settings.hideSelected = false;
const cbOptions : CBOptions = Object.assign({
// so that the user may add different ones as well
className : "tomselect-checkbox",
// the following default to the historic plugin's values
checkedClassNames : undefined,
uncheckedClassNames : undefined,
}, userOptions);
var UpdateChecked = function(checkbox:HTMLInputElement, toCheck : boolean) {
if( toCheck ){
checkbox.checked = true;
if (cbOptions.uncheckedClassNames) {
checkbox.classList.remove(...cbOptions.uncheckedClassNames);
}
if (cbOptions.checkedClassNames) {
checkbox.classList.add(...cbOptions.checkedClassNames);
}
}else{
checkbox.checked = false;
if (cbOptions.checkedClassNames) {
checkbox.classList.remove(...cbOptions.checkedClassNames);
}
if (cbOptions.uncheckedClassNames) {
checkbox.classList.add(...cbOptions.uncheckedClassNames);
}
}
}
// update the checkbox for an option
var UpdateCheckbox = function(option:HTMLElement){
setTimeout(()=>{
var checkbox = option.querySelector('input.' + cbOptions.className);
if( checkbox instanceof HTMLInputElement ){
UpdateChecked(checkbox, option.classList.contains('selected'));
}
},1);
};
// add checkbox to option template
self.hook('after','setupTemplates',() => {
var orig_render_option = self.settings.render.option;
self.settings.render.option = ((data, escape_html) => {
var rendered = getDom(orig_render_option.call(self, data, escape_html));
var checkbox = document.createElement('input');
if (cbOptions.className) {
checkbox.classList.add(cbOptions.className);
}
checkbox.addEventListener('click',function(evt){
preventDefault(evt);
});
checkbox.type = 'checkbox';
const hashed = hash_key(data[self.settings.valueField]);
UpdateChecked(checkbox, !!(hashed && self.items.indexOf(hashed) > -1) );
rendered.prepend(checkbox);
return rendered;
}) satisfies TomTemplate;
});
// uncheck when item removed
self.on('item_remove',(value:string) => {
var option = self.getOption(value);
if( option ){ // if dropdown hasn't been opened yet, the option won't exist
option.classList.remove('selected'); // selected class won't be removed yet
UpdateCheckbox(option);
}
});
// check when item added
self.on('item_add',(value:string) => {
var option = self.getOption(value);
if( option ){ // if dropdown hasn't been opened yet, the option won't exist
UpdateCheckbox(option);
}
});
// remove items when selected option is clicked
self.hook('instead','onOptionSelect',( evt:KeyboardEvent, option:HTMLElement )=>{
if( option.classList.contains('selected') ){
option.classList.remove('selected')
self.removeItem(option.dataset.value);
self.refreshOptions();
preventDefault(evt,true);
return;
}
orig_onOptionSelect.call(self, evt, option);
UpdateCheckbox(option);
});
};

View File

@@ -0,0 +1,15 @@
export type CBOptions = {
/**
* a unique class name for the checkbox to find the input
*/
className ?: string;
/**
* class name to add if checkbox is checked and remove otherwise
*/
checkedClassNames ?: string[],
/**
* class name to add if checkbox was not checked and remove otherwise
*/
uncheckedClassNames ?: string[],
};

View File

@@ -0,0 +1,33 @@
/* stylelint-disable function-name-case */
.plugin-clear_button {
--ts-pr-clear-button: 1em;
.clear-button{
opacity: 0;
position: absolute;
top: 50%;
transform: translateY(-50%);
right: calc(#{$select-padding-x} - #{$select-padding-item-x});
margin-right: 0 !important;
background: transparent !important;
transition: opacity 0.5s;
cursor: pointer;
}
&.form-select .clear-button,
&.single .clear-button {
@if variable-exists(select-padding-dropdown-item-x) {
right: Max(var(--ts-pr-caret), #{$select-padding-dropdown-item-x});
}
@else{
right: Max(var(--ts-pr-caret), calc(#{$select-padding-x} - #{$select-padding-item-x}));
}
}
&.focus.has-items .clear-button,
&:not(.disabled):hover.has-items .clear-button{
opacity: 1;
}
}

View File

@@ -0,0 +1,49 @@
/**
* Plugin: "dropdown_header" (Tom Select)
* Copyright (c) contributors
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this
* file except in compliance with the License. You may obtain a copy of the License at:
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under
* the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF
* ANY KIND, either express or implied. See the License for the specific language
* governing permissions and limitations under the License.
*
*/
import type TomSelect from '../../tom-select.ts';
import { getDom } from '../../vanilla.ts';
import { CBOptions } from './types.ts';
export default function(this:TomSelect, userOptions:CBOptions) {
const self = this;
const options = Object.assign({
className: 'clear-button',
title: 'Clear All',
html: (data:CBOptions) => {
return `<div class="${data.className}" title="${data.title}">&#10799;</div>`;
}
}, userOptions);
self.on('initialize',()=>{
var button = getDom(options.html(options));
button.addEventListener('click',(evt)=>{
if( self.isLocked ) return;
self.clear();
if( self.settings.mode === 'single' && self.settings.allowEmptyOption ){
self.addItem('');
}
evt.preventDefault();
evt.stopPropagation();
});
self.control.appendChild(button);
});
};

View File

@@ -0,0 +1,6 @@
export type CBOptions = {
className ?:string,
title ?:string,
html ?: (data:CBOptions) => string,
}

View File

@@ -0,0 +1,10 @@
.#{$select-ns}-wrapper.plugin-drag_drop {
.ts-dragging{
color:transparent !important;
}
.ts-dragging > * {
visibility:hidden !important;
}
}

143
node_modules/tom-select/src/plugins/drag_drop/plugin.ts generated vendored Normal file
View File

@@ -0,0 +1,143 @@
/**
* Plugin: "drag_drop" (Tom Select)
* Copyright (c) contributors
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this
* file except in compliance with the License. You may obtain a copy of the License at:
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under
* the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF
* ANY KIND, either express or implied. See the License for the specific language
* governing permissions and limitations under the License.
*
*/
import type TomSelect from '../../tom-select.ts';
import { TomOption, TomItem } from '../../types/index.ts';
import { escape_html, preventDefault, addEvent } from '../../utils.ts';
import { getDom, setAttr } from '../../vanilla.ts';
const insertAfter = (referenceNode:Element, newNode:Element) => {
referenceNode.parentNode?.insertBefore(newNode, referenceNode.nextSibling);
}
const insertBefore = (referenceNode:Element, newNode:Element) => {
referenceNode.parentNode?.insertBefore(newNode, referenceNode);
}
const isBefore = (referenceNode:Element|undefined|null, newNode:Element|undefined|null) =>{
do{
newNode = newNode?.previousElementSibling;
if( referenceNode == newNode ){
return true;
}
}while( newNode && newNode.previousElementSibling );
return false;
}
export default function(this:TomSelect) {
var self = this;
if (self.settings.mode !== 'multi') return;
var orig_lock = self.lock;
var orig_unlock = self.unlock;
let sortable = true;
let drag_item:TomItem|undefined;
/**
* Add draggable attribute to item
*/
self.hook('after','setupTemplates',() => {
var orig_render_item = self.settings.render.item;
self.settings.render.item = (data:TomOption, escape:typeof escape_html) => {
const item = getDom(orig_render_item.call(self, data, escape)) as TomItem;
setAttr(item,{'draggable':'true'});
// prevent doc_mousedown (see tom-select.ts)
const mousedown = (evt:Event) => {
if( !sortable ) preventDefault(evt);
evt.stopPropagation();
}
const dragStart = (evt:Event) => {
drag_item = item;
setTimeout(() => {
item.classList.add('ts-dragging');
}, 0);
}
const dragOver = (evt:Event) =>{
evt.preventDefault();
item.classList.add('ts-drag-over');
moveitem(item,drag_item);
}
const dragLeave = () => {
item.classList.remove('ts-drag-over');
}
const moveitem = (targetitem:TomItem, dragitem:TomItem|undefined) => {
if( dragitem === undefined ) return;
if( isBefore(dragitem,item) ){
insertAfter(targetitem,dragitem);
}else{
insertBefore(targetitem,dragitem);
}
}
const dragend = () => {
document.querySelectorAll('.ts-drag-over').forEach(el=> el.classList.remove('ts-drag-over'));
drag_item?.classList.remove('ts-dragging');
drag_item = undefined;
var values:string[] = [];
self.control.querySelectorAll(`[data-value]`).forEach((el:Element)=> {
if( (<HTMLOptionElement>el).dataset.value ){
let value = (<HTMLOptionElement>el).dataset.value;
if( value ){
values.push(value);
}
}
});
self.setValue(values);
}
addEvent(item,'mousedown', mousedown);
addEvent(item,'dragstart', dragStart);
addEvent(item,'dragenter', dragOver)
addEvent(item,'dragover', dragOver);
addEvent(item,'dragleave', dragLeave);
addEvent(item,'dragend', dragend);
return item;
}
});
self.hook('instead','lock',()=>{
sortable = false;
return orig_lock.call(self);
});
self.hook('instead','unlock',()=>{
sortable = true;
return orig_unlock.call(self);
});
};

View File

@@ -0,0 +1,24 @@
.#{$select-ns}-wrapper{
.dropdown-header {
position: relative;
padding: ($select-padding-dropdown-item-y * 2) $select-padding-dropdown-item-x;
border-bottom: 1px solid $select-color-border;
background: color-mix($select-color-dropdown, $select-color-border, 85%);
border-radius: $select-border-radius $select-border-radius 0 0;
}
.dropdown-header-close {
position: absolute;
right: $select-padding-dropdown-item-x;
top: 50%;
color: $select-color-text;
opacity: 0.4;
margin-top: -12px;
line-height: 20px;
font-size: 20px !important;
}
.dropdown-header-close:hover {
color: darken($select-color-text, 25%);
}
}

View File

@@ -0,0 +1,57 @@
/**
* Plugin: "dropdown_header" (Tom Select)
* Copyright (c) contributors
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this
* file except in compliance with the License. You may obtain a copy of the License at:
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under
* the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF
* ANY KIND, either express or implied. See the License for the specific language
* governing permissions and limitations under the License.
*
*/
import type TomSelect from '../../tom-select.ts';
import { getDom } from '../../vanilla.ts';
import { preventDefault } from '../../utils.ts';
import { DHOptions } from './types.ts';
export default function(this:TomSelect, userOptions:DHOptions) {
const self = this;
const options = Object.assign({
title : 'Untitled',
headerClass : 'dropdown-header',
titleRowClass : 'dropdown-header-title',
labelClass : 'dropdown-header-label',
closeClass : 'dropdown-header-close',
html: (data:DHOptions) => {
return (
'<div class="' + data.headerClass + '">' +
'<div class="' + data.titleRowClass + '">' +
'<span class="' + data.labelClass + '">' + data.title + '</span>' +
'<a class="' + data.closeClass + '">&times;</a>' +
'</div>' +
'</div>'
);
}
}, userOptions);
self.on('initialize',()=>{
var header = getDom(options.html(options));
var close_link = header.querySelector('.'+options.closeClass);
if( close_link ){
close_link.addEventListener('click',(evt)=>{
preventDefault(evt,true);
self.close();
});
}
self.dropdown.insertBefore(header, self.dropdown.firstChild);
});
};

View File

@@ -0,0 +1,9 @@
export type DHOptions = {
title ?: string,
headerClass ?: string,
titleRowClass ?: string,
labelClass ?: string,
closeClass ?: string,
html ?: (data:DHOptions) => string,
};

View File

@@ -0,0 +1,43 @@
.plugin-dropdown_input{
&.focus.dropdown-active .#{$select-ns}-control{
box-shadow: none;
border: $select-border;
@if variable-exists(input-box-shadow) {
box-shadow: $input-box-shadow;
}
}
.dropdown-input {
border: 1px solid $select-color-border;
border-width: 0 0 1px;
display: block;
padding: $select-padding-y $select-padding-x;
box-shadow: $select-shadow-input;
width: 100%;
background: transparent;
}
&.focus .#{$select-ns}-dropdown .dropdown-input{
@if variable-exists(input-focus-border-color) {
border-color: $input-focus-border-color;
outline: 0;
@if $enable-shadows {
box-shadow: $input-box-shadow, $input-focus-box-shadow;
} @else {
box-shadow: $input-focus-box-shadow;
}
}
}
.items-placeholder{
border: 0 none !important;
box-shadow: none !important;
width: 100%;
}
&.has-items .items-placeholder,
&.dropdown-active .items-placeholder{
display: none !important;
}
}

View File

@@ -0,0 +1,92 @@
/**
* Plugin: "dropdown_input" (Tom Select)
* Copyright (c) contributors
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this
* file except in compliance with the License. You may obtain a copy of the License at:
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under
* the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF
* ANY KIND, either express or implied. See the License for the specific language
* governing permissions and limitations under the License.
*
*/
import type TomSelect from '../../tom-select.ts';
import * as constants from '../../constants.ts';
import { getDom, addClasses } from '../../vanilla.ts';
import { addEvent, preventDefault } from '../../utils.ts';
export default function(this:TomSelect) {
const self = this;
self.settings.shouldOpen = true; // make sure the input is shown even if there are no options to display in the dropdown
self.hook('before','setup',()=>{
self.focus_node = self.control;
addClasses( self.control_input, 'dropdown-input');
const div = getDom('<div class="dropdown-input-wrap">');
div.append(self.control_input);
self.dropdown.insertBefore(div, self.dropdown.firstChild);
// set a placeholder in the select control
const placeholder = getDom('<input class="items-placeholder" tabindex="-1" />') as HTMLInputElement;
placeholder.placeholder = self.settings.placeholder ||'';
self.control.append(placeholder);
});
self.on('initialize',()=>{
// set tabIndex on control to -1, otherwise [shift+tab] will put focus right back on control_input
self.control_input.addEventListener('keydown',(evt:KeyboardEvent) =>{
//addEvent(self.control_input,'keydown' as const,(evt:KeyboardEvent) =>{
switch( evt.keyCode ){
case constants.KEY_ESC:
if (self.isOpen) {
preventDefault(evt,true);
self.close();
}
self.clearActiveItems();
return;
case constants.KEY_TAB:
self.focus_node.tabIndex = -1;
break;
}
return self.onKeyDown.call(self,evt);
});
self.on('blur',()=>{
self.focus_node.tabIndex = self.isDisabled ? -1 : self.tabIndex;
});
// give the control_input focus when the dropdown is open
self.on('dropdown_open',() =>{
self.control_input.focus();
});
// prevent onBlur from closing when focus is on the control_input
const orig_onBlur = self.onBlur;
self.hook('instead','onBlur',(evt?:FocusEvent)=>{
if( evt && evt.relatedTarget == self.control_input ) return;
return orig_onBlur.call(self);
});
addEvent(self.control_input,'blur', () => self.onBlur() );
// return focus to control to allow further keyboard input
self.hook('before','close',() =>{
if( !self.isOpen ) return;
self.focus_node.focus({preventScroll: true});
});
});
};

View File

@@ -0,0 +1,15 @@
.#{$select-ns}-wrapper.plugin-input_autogrow{
&.has-items .#{$select-ns}-control > input {
min-width: 0;
}
&.has-items.focus .#{$select-ns}-control > input {
flex: none;
min-width: 4px;
&::placeholder {
color:transparent;
}
}
}

View File

@@ -0,0 +1,56 @@
/**
* Plugin: "input_autogrow" (Tom Select)
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this
* file except in compliance with the License. You may obtain a copy of the License at:
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under
* the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF
* ANY KIND, either express or implied. See the License for the specific language
* governing permissions and limitations under the License.
*
*/
import type TomSelect from '../../tom-select.ts';
import { addEvent } from '../../utils.ts';
export default function(this:TomSelect) {
var self = this;
self.on('initialize',()=>{
var test_input = document.createElement('span');
var control = self.control_input;
test_input.style.cssText = 'position:absolute; top:-99999px; left:-99999px; width:auto; padding:0; white-space:pre; ';
self.wrapper.appendChild(test_input);
var transfer_styles = [ 'letterSpacing', 'fontSize', 'fontFamily', 'fontWeight', 'textTransform' ];
for( const style_name of transfer_styles ){
// @ts-ignore TS7015 https://stackoverflow.com/a/50506154/697576
test_input.style[style_name] = control.style[style_name];
}
/**
* Set the control width
*
*/
var resize = ()=>{
test_input.textContent = control.value;
control.style.width = test_input.clientWidth+'px';
};
resize();
self.on('update item_add item_remove',resize);
addEvent(control,'input', resize );
addEvent(control,'keyup', resize );
addEvent(control,'blur', resize );
addEvent(control,'update', resize );
});
};

View File

@@ -0,0 +1,20 @@
/**
* Plugin: "no_active_items" (Tom Select)
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this
* file except in compliance with the License. You may obtain a copy of the License at:
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under
* the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF
* ANY KIND, either express or implied. See the License for the specific language
* governing permissions and limitations under the License.
*
*/
import type TomSelect from '../../tom-select.ts';
export default function(this:TomSelect) {
this.hook('instead','setActiveItem',() => {});
this.hook('instead','selectAll',() => {});
};

View File

@@ -0,0 +1,30 @@
/**
* Plugin: "input_autogrow" (Tom Select)
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this
* file except in compliance with the License. You may obtain a copy of the License at:
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under
* the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF
* ANY KIND, either express or implied. See the License for the specific language
* governing permissions and limitations under the License.
*
*/
import type TomSelect from '../../tom-select.ts';
export default function(this:TomSelect) {
var self = this;
var orig_deleteSelection = self.deleteSelection;
this.hook('instead','deleteSelection',(evt:KeyboardEvent) => {
if( self.activeItems.length ){
return orig_deleteSelection.call(self, evt);
}
return false;
});
};

View File

@@ -0,0 +1,25 @@
.#{$select-ns}-dropdown.plugin-optgroup_columns {
.ts-dropdown-content{
display: flex;
}
.optgroup {
border-right: 1px solid #f2f2f2;
border-top: 0 none;
flex-grow: 1;
flex-basis: 0;
min-width: 0;
}
.optgroup:last-child {
border-right: 0 none;
}
.optgroup::before {
display: none;
}
.optgroup-header {
border-top: 0 none;
}
}

View File

@@ -0,0 +1,59 @@
/**
* Plugin: "optgroup_columns" (Tom Select.js)
* Copyright (c) contributors
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this
* file except in compliance with the License. You may obtain a copy of the License at:
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under
* the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF
* ANY KIND, either express or implied. See the License for the specific language
* governing permissions and limitations under the License.
*
*/
import type TomSelect from '../../tom-select.ts';
import * as constants from '../../constants.ts';
import { parentMatch, nodeIndex } from '../../vanilla.ts';
export default function(this:TomSelect) {
var self = this;
var orig_keydown = self.onKeyDown;
self.hook('instead','onKeyDown',(evt:KeyboardEvent)=>{
var index, option, options, optgroup;
if( !self.isOpen || !(evt.keyCode === constants.KEY_LEFT || evt.keyCode === constants.KEY_RIGHT)) {
return orig_keydown.call(self,evt);
}
self.ignoreHover = true;
optgroup = parentMatch(self.activeOption,'[data-group]');
index = nodeIndex(self.activeOption,'[data-selectable]');
if( !optgroup ){
return;
}
if( evt.keyCode === constants.KEY_LEFT ){
optgroup = optgroup.previousSibling;
} else {
optgroup = optgroup.nextSibling;
}
if( !optgroup ){
return;
}
options = (<HTMLOptGroupElement>optgroup).querySelectorAll('[data-selectable]');
option = options[ Math.min(options.length - 1, index) ] as HTMLElement;
if( option ){
self.setActiveOption(option);
}
});
};

View File

@@ -0,0 +1,70 @@
.#{$select-ns}-wrapper.plugin-remove_button{
.item {
display: inline-flex;
align-items: center;
}
.item .remove {
color: inherit;
text-decoration: none;
vertical-align: middle;
display: inline-block;
padding: 0 $select-padding-item-x;
border-radius: 0 2px 2px 0;
box-sizing: border-box;
}
.item .remove:hover {
background: rgba(0, 0, 0, 5%);
}
&.disabled .item .remove:hover {
background: none;
}
.remove-single {
position: absolute;
right: 0;
top: 0;
font-size: 23px;
}
}
.#{$select-ns}-wrapper.plugin-remove_button:not(.rtl){
.item {
padding-right: 0 !important;
}
.item .remove {
border-left: 1px solid $select-color-item-border;
margin-left: $select-padding-item-x;
}
.item.active .remove {
border-left-color: $select-color-item-active-border;
}
&.disabled .item .remove {
border-left-color: lighten(desaturate($select-color-item-border, 100%), $select-lighten-disabled-item-border);
}
}
.#{$select-ns}-wrapper.plugin-remove_button.rtl {
.item {
padding-left: 0 !important;
}
.item .remove {
border-right: 1px solid $select-color-item-border;
margin-right: $select-padding-item-x;
}
.item.active .remove {
border-right-color: $select-color-item-active-border;
}
&.disabled .item .remove {
border-right-color: lighten(desaturate($select-color-item-border, 100%), $select-lighten-disabled-item-border);
}
}

View File

@@ -0,0 +1,78 @@
/**
* Plugin: "remove_button" (Tom Select)
* Copyright (c) contributors
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this
* file except in compliance with the License. You may obtain a copy of the License at:
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under
* the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF
* ANY KIND, either express or implied. See the License for the specific language
* governing permissions and limitations under the License.
*
*/
import type TomSelect from '../../tom-select.ts';
import { getDom } from '../../vanilla.ts';
import { escape_html, preventDefault, addEvent } from '../../utils.ts';
import { TomOption, TomItem } from '../../types/index.ts';
import { RBOptions } from './types.ts';
export default function(this:TomSelect, userOptions:RBOptions) {
const options = Object.assign({
label : '&times;',
title : 'Remove',
className : 'remove',
append : true
}, userOptions);
//options.className = 'remove-single';
var self = this;
// override the render method to add remove button to each item
if( !options.append ){
return;
}
var html = '<a href="javascript:void(0)" class="' + options.className + '" tabindex="-1" title="' + escape_html(options.title) + '">' + options.label + '</a>';
self.hook('after','setupTemplates',() => {
var orig_render_item = self.settings.render.item;
self.settings.render.item = (data:TomOption, escape:typeof escape_html) => {
var item = getDom(orig_render_item.call(self, data, escape)) as TomItem;
var close_button = getDom(html);
item.appendChild(close_button);
addEvent(close_button,'mousedown',(evt) => {
preventDefault(evt,true);
});
addEvent(close_button,'click',(evt) => {
if( self.isLocked ) return;
// propagating will trigger the dropdown to show for single mode
preventDefault(evt,true);
if( self.isLocked ) return;
if( !self.shouldDelete([item],evt as MouseEvent) ) return;
self.removeItem(item);
self.refreshOptions(false);
self.inputState();
});
return item;
};
});
};

View File

@@ -0,0 +1,7 @@
export type RBOptions = {
label ?: string,
title ?: string,
className ?: string,
append ?: boolean
};

View File

@@ -0,0 +1,44 @@
/**
* Plugin: "restore_on_backspace" (Tom Select)
* Copyright (c) contributors
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this
* file except in compliance with the License. You may obtain a copy of the License at:
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under
* the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF
* ANY KIND, either express or implied. See the License for the specific language
* governing permissions and limitations under the License.
*
*/
import type TomSelect from '../../tom-select.ts';
import { TomOption } from '../../types/index.ts';
type TPluginOptions = {
text?:(option:TomOption)=>string,
};
export default function(this:TomSelect, userOptions:TPluginOptions) {
const self = this;
const options = Object.assign({
text: (option:TomOption) => {
return option[self.settings.labelField];
}
},userOptions);
self.on('item_remove',function(value:string){
if( !self.isFocused ){
return;
}
if( self.control_input.value.trim() === '' ){
var option = self.options[value];
if( option ){
self.setTextboxValue(options.text.call(self, option));
}
}
});
};

View File

@@ -0,0 +1,219 @@
/**
* Plugin: "restore_on_backspace" (Tom Select)
* Copyright (c) contributors
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this
* file except in compliance with the License. You may obtain a copy of the License at:
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under
* the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF
* ANY KIND, either express or implied. See the License for the specific language
* governing permissions and limitations under the License.
*
*/
import type TomSelect from '../../tom-select.ts';
import { TomOption } from '../../types/index.ts';
import { addClasses } from '../../vanilla.ts';
export default function(this:TomSelect) {
const self = this;
const orig_canLoad = self.canLoad;
const orig_clearActiveOption = self.clearActiveOption;
const orig_loadCallback = self.loadCallback;
var pagination:{[key:string]:any} = {};
var dropdown_content:HTMLElement;
var loading_more = false;
var load_more_opt:HTMLElement;
var default_values: string[] = [];
if( !self.settings.shouldLoadMore ){
// return true if additional results should be loaded
self.settings.shouldLoadMore = ():boolean=>{
const scroll_percent = dropdown_content.clientHeight / (dropdown_content.scrollHeight - dropdown_content.scrollTop);
if( scroll_percent > 0.9 ){
return true;
}
if( self.activeOption ){
var selectable = self.selectable();
var index = Array.from(selectable).indexOf(self.activeOption);
if( index >= (selectable.length-2) ){
return true;
}
}
return false;
}
}
if( !self.settings.firstUrl ){
throw 'virtual_scroll plugin requires a firstUrl() method';
}
// in order for virtual scrolling to work,
// options need to be ordered the same way they're returned from the remote data source
self.settings.sortField = [{field:'$order'},{field:'$score'}];
// can we load more results for given query?
const canLoadMore = (query:string):boolean => {
if( typeof self.settings.maxOptions === 'number' && dropdown_content.children.length >= self.settings.maxOptions ){
return false;
}
if( (query in pagination) && pagination[query] ){
return true;
}
return false;
};
const clearFilter = (option:TomOption, value:string):boolean => {
if( self.items.indexOf(value) >= 0 || default_values.indexOf(value) >= 0 ){
return true;
}
return false;
};
// set the next url that will be
self.setNextUrl = (value:string,next_url:any):void => {
pagination[value] = next_url;
};
// getUrl() to be used in settings.load()
self.getUrl = (query:string):any =>{
if( query in pagination ){
const next_url = pagination[query];
pagination[query] = false;
return next_url;
}
// if the user goes back to a previous query
// we need to load the first page again
self.clearPagination();
return self.settings.firstUrl.call(self,query);
};
// clear pagination
self.clearPagination = ():void =>{
pagination = {};
};
// don't clear the active option (and cause unwanted dropdown scroll)
// while loading more results
self.hook('instead','clearActiveOption',()=>{
if( loading_more ){
return;
}
return orig_clearActiveOption.call(self);
});
// override the canLoad method
self.hook('instead','canLoad',(query:string)=>{
// first time the query has been seen
if( !(query in pagination) ){
return orig_canLoad.call(self,query);
}
return canLoadMore(query);
});
// wrap the load
self.hook('instead','loadCallback',( options:TomOption[], optgroups:TomOption[])=>{
if( !loading_more ){
self.clearOptions(clearFilter);
}else if( load_more_opt ){
const first_option = options[0];
if( first_option !== undefined ){
load_more_opt.dataset.value = first_option[self.settings.valueField];
}
}
orig_loadCallback.call( self, options, optgroups);
loading_more = false;
});
// add templates to dropdown
// loading_more if we have another url in the queue
// no_more_results if we don't have another url in the queue
self.hook('after','refreshOptions',()=>{
const query = self.lastValue;
var option;
if( canLoadMore(query) ){
option = self.render('loading_more',{query:query});
if( option ){
option.setAttribute('data-selectable',''); // so that navigating dropdown with [down] keypresses can navigate to this node
load_more_opt = option;
}
}else if( (query in pagination) && !dropdown_content.querySelector('.no-results') ){
option = self.render('no_more_results',{query:query});
}
if( option ){
addClasses(option,self.settings.optionClass);
dropdown_content.append( option );
}
});
// add scroll listener and default templates
self.on('initialize',()=>{
default_values = Object.keys(self.options);
dropdown_content = self.dropdown_content;
// default templates
self.settings.render = Object.assign({}, {
loading_more:() => {
return `<div class="loading-more-results">Loading more results ... </div>`;
},
no_more_results:() =>{
return `<div class="no-more-results">No more results</div>`;
}
},self.settings.render);
// watch dropdown content scroll position
dropdown_content.addEventListener('scroll',()=>{
if( !self.settings.shouldLoadMore.call(self) ){
return;
}
// !important: this will get checked again in load() but we still need to check here otherwise loading_more will be set to true
if( !canLoadMore(self.lastValue) ){
return;
}
// don't call load() too much
if( loading_more ) return;
loading_more = true;
self.load.call(self,self.lastValue);
});
});
};