mirror of
https://github.com/Theodor-Springmann-Stiftung/musenalm.git
synced 2025-10-29 01:05:32 +00:00
select element completion
This commit is contained in:
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
118
views/transform/easymde-example.js
Normal file
118
views/transform/easymde-example.js
Normal file
@@ -0,0 +1,118 @@
|
|||||||
|
document.addEventListener("DOMContentLoaded", function () {
|
||||||
|
console.log("DOM fully loaded. Initializing EasyMDE, custom extension, and CodeMirror overlay.");
|
||||||
|
|
||||||
|
// 1. Custom Marked.js extension (for PREVIEW)
|
||||||
|
const smallCapsExtension = {
|
||||||
|
name: "smallCapsDollar",
|
||||||
|
level: "inline",
|
||||||
|
start(src) {
|
||||||
|
const dollarIndex = src.indexOf("$");
|
||||||
|
if (dollarIndex !== -1 && src[dollarIndex + 1] !== "$") return dollarIndex;
|
||||||
|
return undefined;
|
||||||
|
},
|
||||||
|
tokenizer(src, tokens) {
|
||||||
|
const rule = /^\$([^$\n]+?)\$/;
|
||||||
|
const match = rule.exec(src);
|
||||||
|
if (match) {
|
||||||
|
const textContent = match[1].trim();
|
||||||
|
return { type: "smallCapsDollar", raw: match[0], text: textContent, tokens: [] };
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
},
|
||||||
|
renderer(token) {
|
||||||
|
return `<span class="rendered-small-caps">${token.text}</span>`;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// 2. CodeMirror Overlay Mode (for LIVE EDITING styling)
|
||||||
|
const smallCapsOverlay = {
|
||||||
|
token: function (stream, state) {
|
||||||
|
// Regex to match $text$
|
||||||
|
// It should match a '$', then non-'$' characters, then a closing '$'.
|
||||||
|
// The 'true' argument to stream.match consumes the matched text.
|
||||||
|
if (stream.match(/^\$[^$\n]+?\$/, true)) {
|
||||||
|
return "custom-small-caps"; // This will apply the .cm-custom-small-caps class
|
||||||
|
}
|
||||||
|
|
||||||
|
// If no match, advance the stream until a potential start of our pattern or EOL
|
||||||
|
// This is crucial to prevent infinite loops on non-matching characters.
|
||||||
|
while (stream.next() != null && !stream.match(/^\$[^$\n]+?\$/, false, false)) {
|
||||||
|
// The 'false, false' means: don't consume, case-insensitive is false (though $ isn't case sensitive)
|
||||||
|
// This loop advances char by char if our pattern isn't found immediately.
|
||||||
|
}
|
||||||
|
return null; // No special styling for other characters
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
const easyMDE = new EasyMDE({
|
||||||
|
element: document.getElementById("my-markdown-editor"),
|
||||||
|
initialValue:
|
||||||
|
'# Live Small Caps Editing!\n\nThis text includes $small caps$ which should appear styled $while you type$.\n\nTry the "$S$" button too!',
|
||||||
|
spellChecker: false,
|
||||||
|
placeholder: "Type your Markdown here...",
|
||||||
|
renderingConfig: {
|
||||||
|
markedOptions: {
|
||||||
|
extensions: [smallCapsExtension],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
toolbar: [
|
||||||
|
"bold",
|
||||||
|
"italic",
|
||||||
|
"heading",
|
||||||
|
"|",
|
||||||
|
"quote",
|
||||||
|
"unordered-list",
|
||||||
|
"ordered-list",
|
||||||
|
"|",
|
||||||
|
"link",
|
||||||
|
"image",
|
||||||
|
"|",
|
||||||
|
{
|
||||||
|
name: "insert-small-caps-syntax",
|
||||||
|
action: function (editor) {
|
||||||
|
const cm = editor.codemirror;
|
||||||
|
const selectedText = cm.getSelection();
|
||||||
|
if (selectedText) cm.replaceSelection(`$${selectedText}$`);
|
||||||
|
else {
|
||||||
|
cm.replaceSelection("$$");
|
||||||
|
const cursorPos = cm.getCursor();
|
||||||
|
cm.setCursor({ line: cursorPos.line, ch: cursorPos.ch - 1 });
|
||||||
|
}
|
||||||
|
cm.focus();
|
||||||
|
},
|
||||||
|
className: "fa fa-dollar-sign",
|
||||||
|
title: "Insert Small Caps Syntax ($text$)",
|
||||||
|
},
|
||||||
|
"|",
|
||||||
|
"preview",
|
||||||
|
"side-by-side",
|
||||||
|
"fullscreen",
|
||||||
|
"|",
|
||||||
|
"guide",
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log("EasyMDE initialized.");
|
||||||
|
|
||||||
|
// 3. Add the overlay mode to CodeMirror instance
|
||||||
|
const cmInstance = easyMDE.codemirror;
|
||||||
|
if (cmInstance) {
|
||||||
|
cmInstance.addOverlay(smallCapsOverlay);
|
||||||
|
console.log("CodeMirror overlay for small caps added.");
|
||||||
|
} else {
|
||||||
|
console.warn("Could not get CodeMirror instance from EasyMDE.");
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Debugging Marked.js extension ---
|
||||||
|
const testMarkdownInput = "Test $small caps preview$.";
|
||||||
|
const renderedHtml = easyMDE.markdown(testMarkdownInput);
|
||||||
|
if (renderedHtml.includes('class="rendered-small-caps"')) {
|
||||||
|
console.log("Marked.js extension for preview is working.");
|
||||||
|
} else {
|
||||||
|
console.warn("Marked.js extension for preview might NOT be working.");
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error initializing EasyMDE or applying overlay:", error);
|
||||||
|
}
|
||||||
|
});
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -3,9 +3,11 @@ const MSS_COMPONENT_WRAPPER_CLASS = "mss-component-wrapper";
|
|||||||
const MSS_SELECTED_ITEMS_CONTAINER_CLASS = "mss-selected-items-container";
|
const MSS_SELECTED_ITEMS_CONTAINER_CLASS = "mss-selected-items-container";
|
||||||
const MSS_SELECTED_ITEM_PILL_CLASS = "mss-selected-item-pill";
|
const MSS_SELECTED_ITEM_PILL_CLASS = "mss-selected-item-pill";
|
||||||
const MSS_SELECTED_ITEM_TEXT_CLASS = "mss-selected-item-text";
|
const MSS_SELECTED_ITEM_TEXT_CLASS = "mss-selected-item-text";
|
||||||
|
const MSS_SELECTED_ITEM_PILL_DETAIL_CLASS = "mss-selected-item-pill-detail"; // New class for pill detail
|
||||||
const MSS_SELECTED_ITEM_DELETE_BTN_CLASS = "mss-selected-item-delete-btn";
|
const MSS_SELECTED_ITEM_DELETE_BTN_CLASS = "mss-selected-item-delete-btn";
|
||||||
const MSS_INPUT_CONTROLS_CONTAINER_CLASS = "mss-input-controls-container"; // New container for input and create button
|
const MSS_INPUT_CONTROLS_CONTAINER_CLASS = "mss-input-controls-container";
|
||||||
const MSS_INPUT_WRAPPER_CLASS = "mss-input-wrapper";
|
const MSS_INPUT_WRAPPER_CLASS = "mss-input-wrapper";
|
||||||
|
const MSS_INPUT_WRAPPER_FOCUSED_CLASS = "mss-input-wrapper-focused";
|
||||||
const MSS_TEXT_INPUT_CLASS = "mss-text-input";
|
const MSS_TEXT_INPUT_CLASS = "mss-text-input";
|
||||||
const MSS_CREATE_NEW_BUTTON_CLASS = "mss-create-new-button";
|
const MSS_CREATE_NEW_BUTTON_CLASS = "mss-create-new-button";
|
||||||
const MSS_OPTIONS_LIST_CLASS = "mss-options-list";
|
const MSS_OPTIONS_LIST_CLASS = "mss-options-list";
|
||||||
@@ -14,6 +16,8 @@ const MSS_OPTION_ITEM_NAME_CLASS = "mss-option-item-name";
|
|||||||
const MSS_OPTION_ITEM_DETAIL_CLASS = "mss-option-item-detail";
|
const MSS_OPTION_ITEM_DETAIL_CLASS = "mss-option-item-detail";
|
||||||
const MSS_HIGHLIGHTED_OPTION_CLASS = "mss-option-item-highlighted";
|
const MSS_HIGHLIGHTED_OPTION_CLASS = "mss-option-item-highlighted";
|
||||||
const MSS_HIDDEN_SELECT_CLASS = "mss-hidden-select";
|
const MSS_HIDDEN_SELECT_CLASS = "mss-hidden-select";
|
||||||
|
const MSS_NO_ITEMS_TEXT_CLASS = "mss-no-items-text";
|
||||||
|
|
||||||
// State classes for MultiSelectSimple
|
// State classes for MultiSelectSimple
|
||||||
const MSS_STATE_NO_SELECTION = "mss-state-no-selection";
|
const MSS_STATE_NO_SELECTION = "mss-state-no-selection";
|
||||||
const MSS_STATE_HAS_SELECTION = "mss-state-has-selection";
|
const MSS_STATE_HAS_SELECTION = "mss-state-has-selection";
|
||||||
@@ -26,18 +30,34 @@ export class MultiSelectSimple extends HTMLElement {
|
|||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
this.internals_ = this.attachInternals();
|
this.internals_ = this.attachInternals();
|
||||||
this._value = []; // Array of selected item IDs
|
this._value = [];
|
||||||
this._options = [
|
this._options = [
|
||||||
// Default options
|
{ id: "marie_curie", name: "Marie Curie", additional_data: "Physicist and Chemist" },
|
||||||
{ id: "opt1", name: "Option Alpha", additional_data: "Info A" },
|
{ id: "leonardo_da_vinci", name: "Leonardo da Vinci", additional_data: "Polymath" },
|
||||||
{ id: "opt2", name: "Option Beta", additional_data: "Info B" },
|
{ id: "albert_einstein", name: "Albert Einstein", additional_data: "Theoretical Physicist" },
|
||||||
{ id: "opt3", name: "Option Gamma", additional_data: "Info C" },
|
{ id: "ada_lovelace", name: "Ada Lovelace", additional_data: "Mathematician and Writer" },
|
||||||
{ id: "opt4", name: "Option Delta", additional_data: "Info D" },
|
{ id: "isaac_newton", name: "Isaac Newton", additional_data: "Mathematician and Physicist" },
|
||||||
|
{
|
||||||
|
id: "galileo_galilei",
|
||||||
|
name: "Galileo Galilei",
|
||||||
|
additional_data: "Astronomer and Physicist",
|
||||||
|
},
|
||||||
|
{ id: "charles_darwin", name: "Charles Darwin", additional_data: "Naturalist" },
|
||||||
|
{ id: "nikola_tesla", name: "Nikola Tesla", additional_data: "Inventor and Engineer" },
|
||||||
|
{ id: "jane_austen", name: "Jane Austen", additional_data: "Novelist" },
|
||||||
|
{
|
||||||
|
id: "william_shakespeare",
|
||||||
|
name: "William Shakespeare",
|
||||||
|
additional_data: "Playwright and Poet",
|
||||||
|
},
|
||||||
];
|
];
|
||||||
this._filteredOptions = [];
|
this._filteredOptions = [];
|
||||||
this._highlightedIndex = -1;
|
this._highlightedIndex = -1;
|
||||||
this._isOptionsListVisible = false;
|
this._isOptionsListVisible = false;
|
||||||
|
|
||||||
|
this._placeholder = this.getAttribute("placeholder") || "Search items...";
|
||||||
|
this._showCreateButton = this.getAttribute("show-create-button") !== "false";
|
||||||
|
|
||||||
this._setupTemplates();
|
this._setupTemplates();
|
||||||
this._bindEventHandlers();
|
this._bindEventHandlers();
|
||||||
}
|
}
|
||||||
@@ -45,17 +65,19 @@ export class MultiSelectSimple extends HTMLElement {
|
|||||||
_setupTemplates() {
|
_setupTemplates() {
|
||||||
this.optionTemplate = document.createElement("template");
|
this.optionTemplate = document.createElement("template");
|
||||||
this.optionTemplate.innerHTML = `
|
this.optionTemplate.innerHTML = `
|
||||||
<li role="option" class="${MSS_OPTION_ITEM_CLASS} px-3 py-2 text-sm cursor-pointer transition-colors duration-75 group">
|
<li role="option" class="${MSS_OPTION_ITEM_CLASS}">
|
||||||
<span data-ref="nameEl" class="${MSS_OPTION_ITEM_NAME_CLASS} font-semibold"></span>
|
<span data-ref="nameEl" class="${MSS_OPTION_ITEM_NAME_CLASS}"></span>
|
||||||
<span data-ref="detailEl" class="${MSS_OPTION_ITEM_DETAIL_CLASS} text-xs ml-2"></span>
|
<span data-ref="detailEl" class="${MSS_OPTION_ITEM_DETAIL_CLASS}"></span>
|
||||||
</li>
|
</li>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
this.selectedItemTemplate = document.createElement("template");
|
this.selectedItemTemplate = document.createElement("template");
|
||||||
|
// Apply new MSS_SELECTED_ITEM_PILL_DETAIL_CLASS to the detail span
|
||||||
this.selectedItemTemplate.innerHTML = `
|
this.selectedItemTemplate.innerHTML = `
|
||||||
<span class="${MSS_SELECTED_ITEM_PILL_CLASS} flex items-center">
|
<span class="${MSS_SELECTED_ITEM_PILL_CLASS} flex items-center">
|
||||||
<span data-ref="textEl" class="${MSS_SELECTED_ITEM_TEXT_CLASS}"></span>
|
<span data-ref="textEl" class="${MSS_SELECTED_ITEM_TEXT_CLASS}"></span>
|
||||||
<button type="button" data-ref="deleteBtn" class="${MSS_SELECTED_ITEM_DELETE_BTN_CLASS} ml-1.5 text-xs">×</button>
|
<span data-ref="detailEl" class="${MSS_SELECTED_ITEM_PILL_DETAIL_CLASS} hidden"></span>
|
||||||
|
<button type="button" data-ref="deleteBtn" class="${MSS_SELECTED_ITEM_DELETE_BTN_CLASS}">×</button>
|
||||||
</span>
|
</span>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
@@ -68,13 +90,34 @@ export class MultiSelectSimple extends HTMLElement {
|
|||||||
this._handleOptionMouseDown = this._handleOptionMouseDown.bind(this);
|
this._handleOptionMouseDown = this._handleOptionMouseDown.bind(this);
|
||||||
this._handleOptionClick = this._handleOptionClick.bind(this);
|
this._handleOptionClick = this._handleOptionClick.bind(this);
|
||||||
this._handleCreateNewButtonClick = this._handleCreateNewButtonClick.bind(this);
|
this._handleCreateNewButtonClick = this._handleCreateNewButtonClick.bind(this);
|
||||||
|
this._handleSelectedItemsContainerClick = this._handleSelectedItemsContainerClick.bind(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
_getItemById(id) {
|
_getItemById(id) {
|
||||||
return this._options.find((opt) => opt.id === id);
|
return this._options.find((opt) => opt.id === id);
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Public Methods ---
|
get placeholder() {
|
||||||
|
return this._placeholder;
|
||||||
|
}
|
||||||
|
set placeholder(value) {
|
||||||
|
this._placeholder = value;
|
||||||
|
if (this.inputElement) this.inputElement.placeholder = this._placeholder;
|
||||||
|
this.setAttribute("placeholder", value);
|
||||||
|
}
|
||||||
|
|
||||||
|
get showCreateButton() {
|
||||||
|
return this._showCreateButton;
|
||||||
|
}
|
||||||
|
set showCreateButton(value) {
|
||||||
|
const boolValue = String(value).toLowerCase() !== "false" && value !== false;
|
||||||
|
if (this._showCreateButton === boolValue) return;
|
||||||
|
this._showCreateButton = boolValue;
|
||||||
|
if (this.createNewButton)
|
||||||
|
this.createNewButton.classList.toggle("hidden", !this._showCreateButton);
|
||||||
|
this.setAttribute("show-create-button", this._showCreateButton ? "true" : "false");
|
||||||
|
}
|
||||||
|
|
||||||
setOptions(newOptions) {
|
setOptions(newOptions) {
|
||||||
if (
|
if (
|
||||||
Array.isArray(newOptions) &&
|
Array.isArray(newOptions) &&
|
||||||
@@ -82,36 +125,38 @@ export class MultiSelectSimple extends HTMLElement {
|
|||||||
) {
|
) {
|
||||||
this._options = [...newOptions];
|
this._options = [...newOptions];
|
||||||
const validValues = this._value.filter((id) => this._getItemById(id));
|
const validValues = this._value.filter((id) => this._getItemById(id));
|
||||||
if (validValues.length !== this._value.length) {
|
if (validValues.length !== this._value.length) this.value = validValues;
|
||||||
this.value = validValues;
|
else if (this.selectedItemsContainer) this._renderSelectedItems();
|
||||||
}
|
|
||||||
this._filteredOptions = [];
|
this._filteredOptions = [];
|
||||||
this._highlightedIndex = -1;
|
this._highlightedIndex = -1;
|
||||||
if (this.inputElement && this.inputElement.value) {
|
if (this.inputElement && this.inputElement.value)
|
||||||
this._handleInput({ target: this.inputElement });
|
this._handleInput({ target: this.inputElement });
|
||||||
} else {
|
else this._hideOptionsList();
|
||||||
this._hideOptionsList();
|
} else console.error("setOptions expects an array of objects with id and name properties.");
|
||||||
}
|
|
||||||
} else {
|
|
||||||
console.error("setOptions expects an array of objects with id and name properties.");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
get value() {
|
get value() {
|
||||||
return this._value;
|
return this._value;
|
||||||
}
|
}
|
||||||
|
|
||||||
set value(newVal) {
|
set value(newVal) {
|
||||||
|
const oldValString = JSON.stringify(this._value.sort());
|
||||||
if (Array.isArray(newVal)) {
|
if (Array.isArray(newVal)) {
|
||||||
this._value = [
|
this._value = [
|
||||||
...new Set(newVal.filter((id) => typeof id === "string" && this._getItemById(id))),
|
...new Set(newVal.filter((id) => typeof id === "string" && this._getItemById(id))),
|
||||||
];
|
];
|
||||||
} else {
|
} else if (typeof newVal === "string" && newVal.trim() !== "") {
|
||||||
this._value = [];
|
const singleId = newVal.trim();
|
||||||
|
if (this._getItemById(singleId) && !this._value.includes(singleId)) this._value = [singleId];
|
||||||
|
else if (!this._getItemById(singleId))
|
||||||
|
this._value = this._value.filter((id) => id !== singleId);
|
||||||
|
} else this._value = [];
|
||||||
|
const newValString = JSON.stringify(this._value.sort());
|
||||||
|
if (oldValString !== newValString) {
|
||||||
|
this._updateFormValue();
|
||||||
|
if (this.selectedItemsContainer) this._renderSelectedItems();
|
||||||
|
this._updateRootElementStateClasses();
|
||||||
|
this.dispatchEvent(new Event("change", { bubbles: true }));
|
||||||
}
|
}
|
||||||
this._updateFormValue();
|
|
||||||
if (this.selectedItemsContainer) this._renderSelectedItems();
|
|
||||||
this._updateRootElementStateClasses();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
get name() {
|
get name() {
|
||||||
@@ -132,6 +177,9 @@ export class MultiSelectSimple extends HTMLElement {
|
|||||||
this.selectedItemsContainer = this.querySelector(`.${MSS_SELECTED_ITEMS_CONTAINER_CLASS}`);
|
this.selectedItemsContainer = this.querySelector(`.${MSS_SELECTED_ITEMS_CONTAINER_CLASS}`);
|
||||||
this.hiddenSelect = this.querySelector(`.${MSS_HIDDEN_SELECT_CLASS}`);
|
this.hiddenSelect = this.querySelector(`.${MSS_HIDDEN_SELECT_CLASS}`);
|
||||||
|
|
||||||
|
this.placeholder = this.getAttribute("placeholder") || "Search items...";
|
||||||
|
this.showCreateButton = this.getAttribute("show-create-button") !== "false";
|
||||||
|
|
||||||
if (this.name && this.hiddenSelect) this.hiddenSelect.name = this.name;
|
if (this.name && this.hiddenSelect) this.hiddenSelect.name = this.name;
|
||||||
|
|
||||||
this.inputElement.addEventListener("input", this._handleInput);
|
this.inputElement.addEventListener("input", this._handleInput);
|
||||||
@@ -141,16 +189,15 @@ export class MultiSelectSimple extends HTMLElement {
|
|||||||
this.optionsListElement.addEventListener("mousedown", this._handleOptionMouseDown);
|
this.optionsListElement.addEventListener("mousedown", this._handleOptionMouseDown);
|
||||||
this.optionsListElement.addEventListener("click", this._handleOptionClick);
|
this.optionsListElement.addEventListener("click", this._handleOptionClick);
|
||||||
this.createNewButton.addEventListener("click", this._handleCreateNewButtonClick);
|
this.createNewButton.addEventListener("click", this._handleCreateNewButtonClick);
|
||||||
|
this.selectedItemsContainer.addEventListener("click", this._handleSelectedItemsContainerClick);
|
||||||
|
|
||||||
this._updateRootElementStateClasses();
|
this._updateRootElementStateClasses();
|
||||||
// Create button is always visible now, visibility logic removed from _handleInput
|
|
||||||
// this.createNewButton.classList.add('hidden'); // No longer initially hidden by default in JS
|
|
||||||
|
|
||||||
if (this.hasAttribute("value")) {
|
if (this.hasAttribute("value")) {
|
||||||
|
const attrValue = this.getAttribute("value");
|
||||||
try {
|
try {
|
||||||
this.value = JSON.parse(this.getAttribute("value"));
|
this.value = JSON.parse(attrValue);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
this.value = this.getAttribute("value")
|
this.value = attrValue
|
||||||
.split(",")
|
.split(",")
|
||||||
.map((s) => s.trim())
|
.map((s) => s.trim())
|
||||||
.filter(Boolean);
|
.filter(Boolean);
|
||||||
@@ -173,12 +220,36 @@ export class MultiSelectSimple extends HTMLElement {
|
|||||||
this.optionsListElement.removeEventListener("mousedown", this._handleOptionMouseDown);
|
this.optionsListElement.removeEventListener("mousedown", this._handleOptionMouseDown);
|
||||||
this.optionsListElement.removeEventListener("click", this._handleOptionClick);
|
this.optionsListElement.removeEventListener("click", this._handleOptionClick);
|
||||||
}
|
}
|
||||||
if (this.createNewButton) {
|
if (this.createNewButton)
|
||||||
this.createNewButton.removeEventListener("click", this._handleCreateNewButtonClick);
|
this.createNewButton.removeEventListener("click", this._handleCreateNewButtonClick);
|
||||||
}
|
if (this.selectedItemsContainer)
|
||||||
|
this.selectedItemsContainer.removeEventListener(
|
||||||
|
"click",
|
||||||
|
this._handleSelectedItemsContainerClick,
|
||||||
|
);
|
||||||
clearTimeout(this._blurTimeout);
|
clearTimeout(this._blurTimeout);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static get observedAttributes() {
|
||||||
|
return ["disabled", "name", "value", "placeholder", "show-create-button"];
|
||||||
|
}
|
||||||
|
attributeChangedCallback(name, oldValue, newValue) {
|
||||||
|
if (oldValue === newValue) return;
|
||||||
|
if (name === "disabled") this.disabledCallback(this.hasAttribute("disabled"));
|
||||||
|
else if (name === "name" && this.hiddenSelect) this.hiddenSelect.name = newValue;
|
||||||
|
else if (name === "value" && this.inputElement) {
|
||||||
|
try {
|
||||||
|
this.value = JSON.parse(newValue);
|
||||||
|
} catch (e) {
|
||||||
|
this.value = newValue
|
||||||
|
.split(",")
|
||||||
|
.map((s) => s.trim())
|
||||||
|
.filter(Boolean);
|
||||||
|
}
|
||||||
|
} else if (name === "placeholder") this.placeholder = newValue;
|
||||||
|
else if (name === "show-create-button") this.showCreateButton = newValue;
|
||||||
|
}
|
||||||
|
|
||||||
formAssociatedCallback(form) {}
|
formAssociatedCallback(form) {}
|
||||||
formDisabledCallback(disabled) {
|
formDisabledCallback(disabled) {
|
||||||
this.disabledCallback(disabled);
|
this.disabledCallback(disabled);
|
||||||
@@ -187,8 +258,10 @@ export class MultiSelectSimple extends HTMLElement {
|
|||||||
this.value = [];
|
this.value = [];
|
||||||
this._hideOptionsList();
|
this._hideOptionsList();
|
||||||
if (this.inputElement) this.inputElement.value = "";
|
if (this.inputElement) this.inputElement.value = "";
|
||||||
// Create button visibility is not tied to reset, it's always shown
|
this.placeholder = this.getAttribute("placeholder") || "Search items...";
|
||||||
|
this.showCreateButton = this.getAttribute("show-create-button") !== "false";
|
||||||
this._updateRootElementStateClasses();
|
this._updateRootElementStateClasses();
|
||||||
|
this._renderSelectedItems();
|
||||||
}
|
}
|
||||||
formStateRestoreCallback(state, mode) {
|
formStateRestoreCallback(state, mode) {
|
||||||
this.value = Array.isArray(state) ? state : [];
|
this.value = Array.isArray(state) ? state : [];
|
||||||
@@ -201,71 +274,72 @@ export class MultiSelectSimple extends HTMLElement {
|
|||||||
this._value.forEach((id) => {
|
this._value.forEach((id) => {
|
||||||
const option = document.createElement("option");
|
const option = document.createElement("option");
|
||||||
option.value = id;
|
option.value = id;
|
||||||
option.textContent = id;
|
const itemData = this._getItemById(id);
|
||||||
|
option.textContent = itemData ? itemData.name : id;
|
||||||
option.selected = true;
|
option.selected = true;
|
||||||
this.hiddenSelect.appendChild(option);
|
this.hiddenSelect.appendChild(option);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
_updateFormValue() {
|
_updateFormValue() {
|
||||||
this.internals_.setFormValue(null);
|
this.internals_.setFormValue(null);
|
||||||
this._synchronizeHiddenSelect();
|
this._synchronizeHiddenSelect();
|
||||||
}
|
}
|
||||||
|
|
||||||
disabledCallback(disabled) {
|
disabledCallback(disabled) {
|
||||||
if (this.inputElement) this.inputElement.disabled = disabled;
|
if (this.inputElement) this.inputElement.disabled = disabled;
|
||||||
if (this.createNewButton) this.createNewButton.disabled = disabled;
|
if (this.createNewButton) this.createNewButton.disabled = disabled;
|
||||||
this.classList.toggle("opacity-50", disabled);
|
this.toggleAttribute("disabled", disabled);
|
||||||
this.classList.toggle("cursor-not-allowed", disabled);
|
|
||||||
if (this.selectedItemsContainer)
|
|
||||||
this.selectedItemsContainer.classList.toggle("pointer-events-none", disabled);
|
|
||||||
this.querySelectorAll(`.${MSS_SELECTED_ITEM_DELETE_BTN_CLASS}`).forEach(
|
this.querySelectorAll(`.${MSS_SELECTED_ITEM_DELETE_BTN_CLASS}`).forEach(
|
||||||
(btn) => (btn.disabled = disabled),
|
(btn) => (btn.disabled = disabled),
|
||||||
);
|
);
|
||||||
if (this.hiddenSelect) this.hiddenSelect.disabled = disabled;
|
if (this.hiddenSelect) this.hiddenSelect.disabled = disabled;
|
||||||
|
if (disabled) this._hideOptionsList();
|
||||||
}
|
}
|
||||||
|
|
||||||
_updateRootElementStateClasses() {
|
_updateRootElementStateClasses() {
|
||||||
this.classList.toggle(MSS_STATE_NO_SELECTION, this._value.length === 0);
|
this.classList.toggle(MSS_STATE_NO_SELECTION, this._value.length === 0);
|
||||||
this.classList.toggle(MSS_STATE_HAS_SELECTION, this._value.length > 0);
|
this.classList.toggle(MSS_STATE_HAS_SELECTION, this._value.length > 0);
|
||||||
this.classList.toggle(MSS_STATE_LIST_OPEN, this._isOptionsListVisible);
|
this.classList.toggle(MSS_STATE_LIST_OPEN, this._isOptionsListVisible);
|
||||||
}
|
}
|
||||||
|
|
||||||
_render() {
|
_render() {
|
||||||
const componentId = this.id || `mss-${crypto.randomUUID().slice(0, 8)}`;
|
const componentId = this.id || `mss-${crypto.randomUUID().slice(0, 8)}`;
|
||||||
if (!this.id) this.setAttribute("id", componentId);
|
if (!this.id) this.setAttribute("id", componentId);
|
||||||
this.innerHTML = `
|
this.innerHTML = `
|
||||||
<style> .${MSS_HIDDEN_SELECT_CLASS} { display: none !important; visibility: hidden !important; position: absolute !important; width: 0 !important; height: 0 !important; opacity: 0 !important; pointer-events: none !important; } </style>
|
<style>
|
||||||
|
.${MSS_HIDDEN_SELECT_CLASS} { display: block !important; visibility: hidden !important; position: absolute !important; width: 0px !important; height: 0px !important; opacity: 0 !important; pointer-events: none !important; margin: -1px !important; padding: 0 !important; border: 0 !important; overflow: hidden !important; clip: rect(0, 0, 0, 0) !important; white-space: nowrap !important; }
|
||||||
|
</style>
|
||||||
<div class="${MSS_COMPONENT_WRAPPER_CLASS} relative">
|
<div class="${MSS_COMPONENT_WRAPPER_CLASS} relative">
|
||||||
<div class="${MSS_SELECTED_ITEMS_CONTAINER_CLASS} flex flex-wrap gap-1 mb-1 min-h-[38px]" aria-live="polite"></div>
|
<div class="${MSS_SELECTED_ITEMS_CONTAINER_CLASS} flex flex-wrap gap-1 mb-1 min-h-[38px]" aria-live="polite" tabindex="-1"></div>
|
||||||
<div class="${MSS_INPUT_CONTROLS_CONTAINER_CLASS} flex items-center space-x-2">
|
<div class="${MSS_INPUT_CONTROLS_CONTAINER_CLASS} flex items-center space-x-2">
|
||||||
<div class="${MSS_INPUT_WRAPPER_CLASS} relative rounded-md flex items-center flex-grow">
|
<div class="${MSS_INPUT_WRAPPER_CLASS} relative rounded-md flex items-center flex-grow">
|
||||||
<input type="text" class="${MSS_TEXT_INPUT_CLASS} w-full outline-none bg-transparent p-1.5 text-sm" placeholder="Search items..."/>
|
<input type="text"
|
||||||
|
class="${MSS_TEXT_INPUT_CLASS} w-full outline-none bg-transparent text-sm"
|
||||||
|
placeholder="${this.placeholder}"
|
||||||
|
aria-autocomplete="list"
|
||||||
|
aria-expanded="${this._isOptionsListVisible}"
|
||||||
|
aria-controls="options-list-${componentId}"
|
||||||
|
autocomplete="off" autocorrect="off" autocapitalize="none" spellcheck="false" role="combobox" />
|
||||||
</div>
|
</div>
|
||||||
<button type="button" class="${MSS_CREATE_NEW_BUTTON_CLASS} px-2 py-1 text-xs rounded-sm"> +
|
<button type="button" class="${MSS_CREATE_NEW_BUTTON_CLASS} ${!this.showCreateButton ? "hidden" : ""}" title="Create new item from input">+</button>
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
<ul role="listbox" class="${MSS_OPTIONS_LIST_CLASS} absolute z-20 w-full max-h-60 overflow-y-auto mt-1 hidden"></ul>
|
<ul id="options-list-${componentId}" role="listbox" class="${MSS_OPTIONS_LIST_CLASS} absolute z-20 w-full max-h-60 overflow-y-auto mt-1 hidden"></ul>
|
||||||
<select multiple name="${this.getAttribute("name") || "simple_items_default"}" id="hidden-select-${componentId}" class="${MSS_HIDDEN_SELECT_CLASS}" aria-hidden="true"></select>
|
<select multiple name="${this.getAttribute("name") || "mss_default_name"}" id="hidden-select-${componentId}" class="${MSS_HIDDEN_SELECT_CLASS}" aria-hidden="true"></select>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
_createSelectedItemElement(itemId) {
|
_createSelectedItemElement(itemId) {
|
||||||
const itemData = this._getItemById(itemId);
|
const itemData = this._getItemById(itemId);
|
||||||
if (!itemData) return null;
|
if (!itemData) return null;
|
||||||
const fragment = this.selectedItemTemplate.content.cloneNode(true);
|
const fragment = this.selectedItemTemplate.content.cloneNode(true);
|
||||||
const pillEl = fragment.firstElementChild;
|
const pillEl = fragment.firstElementChild;
|
||||||
const textEl = pillEl.querySelector('[data-ref="textEl"]');
|
const textEl = pillEl.querySelector('[data-ref="textEl"]');
|
||||||
|
const detailEl = pillEl.querySelector('[data-ref="detailEl"]'); // This now uses MSS_SELECTED_ITEM_PILL_DETAIL_CLASS
|
||||||
|
const deleteBtn = pillEl.querySelector('[data-ref="deleteBtn"]');
|
||||||
textEl.textContent = itemData.name;
|
textEl.textContent = itemData.name;
|
||||||
if (itemData.additional_data) {
|
if (itemData.additional_data) {
|
||||||
const detailSpan = document.createElement("span");
|
detailEl.textContent = `(${itemData.additional_data})`;
|
||||||
detailSpan.className = "ml-1 opacity-75 text-xs";
|
detailEl.classList.remove("hidden"); // Toggle visibility via JS
|
||||||
detailSpan.textContent = `(${itemData.additional_data})`;
|
} else {
|
||||||
textEl.appendChild(detailSpan);
|
detailEl.classList.add("hidden"); // Toggle visibility via JS
|
||||||
}
|
}
|
||||||
|
|
||||||
const deleteBtn = pillEl.querySelector('[data-ref="deleteBtn"]');
|
|
||||||
deleteBtn.setAttribute("aria-label", `Remove ${itemData.name}`);
|
deleteBtn.setAttribute("aria-label", `Remove ${itemData.name}`);
|
||||||
deleteBtn.dataset.id = itemId;
|
deleteBtn.dataset.id = itemId;
|
||||||
deleteBtn.disabled = this.hasAttribute("disabled");
|
deleteBtn.disabled = this.hasAttribute("disabled");
|
||||||
@@ -275,20 +349,19 @@ export class MultiSelectSimple extends HTMLElement {
|
|||||||
});
|
});
|
||||||
return pillEl;
|
return pillEl;
|
||||||
}
|
}
|
||||||
|
|
||||||
_renderSelectedItems() {
|
_renderSelectedItems() {
|
||||||
if (!this.selectedItemsContainer) return;
|
if (!this.selectedItemsContainer) return;
|
||||||
this.selectedItemsContainer.innerHTML = "";
|
this.selectedItemsContainer.innerHTML = "";
|
||||||
this._value.forEach((id) => {
|
|
||||||
const pillEl = this._createSelectedItemElement(id);
|
|
||||||
if (pillEl) this.selectedItemsContainer.appendChild(pillEl);
|
|
||||||
});
|
|
||||||
if (this._value.length === 0) {
|
if (this._value.length === 0) {
|
||||||
this.selectedItemsContainer.innerHTML = `<span class="italic text-xs">No items selected</span>`;
|
this.selectedItemsContainer.innerHTML = `<span class="${MSS_NO_ITEMS_TEXT_CLASS}">No items selected</span>`;
|
||||||
|
} else {
|
||||||
|
this._value.forEach((id) => {
|
||||||
|
const pillEl = this._createSelectedItemElement(id);
|
||||||
|
if (pillEl) this.selectedItemsContainer.appendChild(pillEl);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
this._updateRootElementStateClasses();
|
this._updateRootElementStateClasses();
|
||||||
}
|
}
|
||||||
|
|
||||||
_createOptionElement(itemData, index) {
|
_createOptionElement(itemData, index) {
|
||||||
const fragment = this.optionTemplate.content.cloneNode(true);
|
const fragment = this.optionTemplate.content.cloneNode(true);
|
||||||
const li = fragment.firstElementChild;
|
const li = fragment.firstElementChild;
|
||||||
@@ -298,43 +371,61 @@ export class MultiSelectSimple extends HTMLElement {
|
|||||||
detailEl.textContent = itemData.additional_data ? `(${itemData.additional_data})` : "";
|
detailEl.textContent = itemData.additional_data ? `(${itemData.additional_data})` : "";
|
||||||
li.dataset.id = itemData.id;
|
li.dataset.id = itemData.id;
|
||||||
li.setAttribute("aria-selected", String(index === this._highlightedIndex));
|
li.setAttribute("aria-selected", String(index === this._highlightedIndex));
|
||||||
|
const optionElementId = `option-${this.id || "mss"}-${itemData.id}`;
|
||||||
|
li.id = optionElementId;
|
||||||
if (index === this._highlightedIndex) {
|
if (index === this._highlightedIndex) {
|
||||||
li.classList.add(MSS_HIGHLIGHTED_OPTION_CLASS);
|
li.classList.add(MSS_HIGHLIGHTED_OPTION_CLASS);
|
||||||
li.id = `highlighted-option-${this.id || "multi-select-simple"}`;
|
if (this.inputElement)
|
||||||
|
this.inputElement.setAttribute("aria-activedescendant", optionElementId);
|
||||||
}
|
}
|
||||||
return li;
|
return li;
|
||||||
}
|
}
|
||||||
|
|
||||||
_renderOptionsList() {
|
_renderOptionsList() {
|
||||||
if (!this.optionsListElement) return;
|
if (!this.optionsListElement || !this.inputElement) return;
|
||||||
this.optionsListElement.innerHTML = "";
|
this.optionsListElement.innerHTML = "";
|
||||||
|
this.inputElement.removeAttribute("aria-activedescendant");
|
||||||
if (this._filteredOptions.length === 0 || !this._isOptionsListVisible) {
|
if (this._filteredOptions.length === 0 || !this._isOptionsListVisible) {
|
||||||
this.optionsListElement.classList.add("hidden");
|
this.optionsListElement.classList.add("hidden");
|
||||||
|
this.inputElement.setAttribute("aria-expanded", "false");
|
||||||
} else {
|
} else {
|
||||||
this.optionsListElement.classList.remove("hidden");
|
this.optionsListElement.classList.remove("hidden");
|
||||||
|
this.inputElement.setAttribute("aria-expanded", "true");
|
||||||
this._filteredOptions.forEach((item, index) => {
|
this._filteredOptions.forEach((item, index) => {
|
||||||
const optionEl = this._createOptionElement(item, index);
|
const optionEl = this._createOptionElement(item, index);
|
||||||
this.optionsListElement.appendChild(optionEl);
|
this.optionsListElement.appendChild(optionEl);
|
||||||
});
|
});
|
||||||
const highlightedElement = this.optionsListElement.querySelector(
|
const highlightedElement = this.optionsListElement.querySelector(
|
||||||
`#highlighted-option-${this.id || "multi-select-simple"}`,
|
`.${MSS_HIGHLIGHTED_OPTION_CLASS}`,
|
||||||
);
|
);
|
||||||
if (highlightedElement) highlightedElement.scrollIntoView({ block: "nearest" });
|
if (highlightedElement) {
|
||||||
|
highlightedElement.scrollIntoView({ block: "nearest" });
|
||||||
|
this.inputElement.setAttribute("aria-activedescendant", highlightedElement.id);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
this._updateRootElementStateClasses();
|
this._updateRootElementStateClasses();
|
||||||
}
|
}
|
||||||
|
_handleSelectedItemsContainerClick(event) {
|
||||||
_handleCreateNewButtonClick() {
|
if (
|
||||||
const inputValue = this.inputElement ? this.inputElement.value.trim() : "";
|
event.target === this.selectedItemsContainer &&
|
||||||
console.log(`"Create New" button clicked. Current input value: "${inputValue}"`);
|
this.inputElement &&
|
||||||
// User will implement the actual creation logic here.
|
!this.inputElement.disabled
|
||||||
|
) {
|
||||||
|
this.inputElement.focus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_handleCreateNewButtonClick() {
|
||||||
|
if (this.hasAttribute("disabled") || !this.showCreateButton) return;
|
||||||
|
const inputValue = this.inputElement ? this.inputElement.value.trim() : "";
|
||||||
|
this.dispatchEvent(
|
||||||
|
new CustomEvent("createnew", {
|
||||||
|
detail: { value: inputValue },
|
||||||
|
bubbles: true,
|
||||||
|
composed: true,
|
||||||
|
}),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
_handleInput(event) {
|
_handleInput(event) {
|
||||||
const searchTerm = event.target.value;
|
const searchTerm = event.target.value;
|
||||||
|
|
||||||
// "Create New" button is always visible, no need to toggle based on input here.
|
|
||||||
|
|
||||||
if (searchTerm.length === 0) {
|
if (searchTerm.length === 0) {
|
||||||
this._filteredOptions = [];
|
this._filteredOptions = [];
|
||||||
this._isOptionsListVisible = false;
|
this._isOptionsListVisible = false;
|
||||||
@@ -342,24 +433,33 @@ export class MultiSelectSimple extends HTMLElement {
|
|||||||
const searchTermLower = searchTerm.toLowerCase();
|
const searchTermLower = searchTerm.toLowerCase();
|
||||||
this._filteredOptions = this._options.filter((item) => {
|
this._filteredOptions = this._options.filter((item) => {
|
||||||
if (this._value.includes(item.id)) return false;
|
if (this._value.includes(item.id)) return false;
|
||||||
return (
|
const nameMatch = item.name.toLowerCase().includes(searchTermLower);
|
||||||
item.name.toLowerCase().includes(searchTermLower) ||
|
const additionalDataMatch =
|
||||||
(item.additional_data && item.additional_data.toLowerCase().includes(searchTermLower))
|
item.additional_data && item.additional_data.toLowerCase().includes(searchTermLower);
|
||||||
);
|
return nameMatch || additionalDataMatch;
|
||||||
});
|
});
|
||||||
this._isOptionsListVisible = this._filteredOptions.length > 0;
|
this._isOptionsListVisible = this._filteredOptions.length > 0;
|
||||||
}
|
}
|
||||||
this._highlightedIndex = this._filteredOptions.length > 0 ? 0 : -1;
|
this._highlightedIndex = this._filteredOptions.length > 0 ? 0 : -1;
|
||||||
this._renderOptionsList();
|
this._renderOptionsList();
|
||||||
}
|
}
|
||||||
|
|
||||||
_handleKeyDown(event) {
|
_handleKeyDown(event) {
|
||||||
if (this.inputElement.disabled) return;
|
if (this.inputElement.disabled) return;
|
||||||
if (event.key === "Backspace") {
|
// Removed: Backspace on empty input to delete last item
|
||||||
return;
|
// if (event.key === "Backspace" && this.inputElement.value === "" && this._value.length > 0) {
|
||||||
}
|
// event.preventDefault();
|
||||||
|
// const lastItemId = this._value[this._value.length - 1];
|
||||||
|
// this._handleDeleteSelectedItem(lastItemId);
|
||||||
|
// return;
|
||||||
|
// }
|
||||||
if (!this._isOptionsListVisible || this._filteredOptions.length === 0) {
|
if (!this._isOptionsListVisible || this._filteredOptions.length === 0) {
|
||||||
if (event.key === "Escape") this._hideOptionsList();
|
if (event.key === "Escape") this._hideOptionsList();
|
||||||
|
if (
|
||||||
|
(event.key === "ArrowDown" || event.key === "ArrowUp") &&
|
||||||
|
this.inputElement.value.length > 0
|
||||||
|
) {
|
||||||
|
this._handleInput({ target: this.inputElement });
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
switch (event.key) {
|
switch (event.key) {
|
||||||
@@ -390,128 +490,42 @@ export class MultiSelectSimple extends HTMLElement {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_hideOptionsList() {
|
_hideOptionsList() {
|
||||||
this._isOptionsListVisible = false;
|
this._isOptionsListVisible = false;
|
||||||
this._highlightedIndex = -1;
|
this._highlightedIndex = -1;
|
||||||
if (this.optionsListElement) this._renderOptionsList();
|
if (this.optionsListElement) this._renderOptionsList();
|
||||||
}
|
}
|
||||||
|
|
||||||
_handleFocus() {
|
_handleFocus() {
|
||||||
if (this.inputElement.disabled) return;
|
if (this.inputElement.disabled) return;
|
||||||
if (this.inputWrapper)
|
if (this.inputWrapper) this.inputWrapper.classList.add(MSS_INPUT_WRAPPER_FOCUSED_CLASS);
|
||||||
this.inputWrapper.classList.add(
|
if (this.inputElement.value.length > 0) this._handleInput({ target: this.inputElement });
|
||||||
"border",
|
|
||||||
"border-gray-300",
|
|
||||||
"focus-within:border-blue-500",
|
|
||||||
"focus-within:ring-1",
|
|
||||||
"focus-within:ring-blue-500",
|
|
||||||
);
|
|
||||||
|
|
||||||
if (this.inputElement.value.length > 0) {
|
|
||||||
this._handleInput({ target: this.inputElement });
|
|
||||||
} else {
|
|
||||||
this._hideOptionsList();
|
|
||||||
}
|
|
||||||
this._updateRootElementStateClasses();
|
this._updateRootElementStateClasses();
|
||||||
}
|
}
|
||||||
|
|
||||||
_blurTimeout = null;
|
_blurTimeout = null;
|
||||||
_handleBlur() {
|
_handleBlur() {
|
||||||
if (this.inputWrapper)
|
if (this.inputWrapper) this.inputWrapper.classList.remove(MSS_INPUT_WRAPPER_FOCUSED_CLASS);
|
||||||
this.inputWrapper.classList.remove(
|
|
||||||
"border",
|
|
||||||
"border-gray-300",
|
|
||||||
"focus-within:border-blue-500",
|
|
||||||
"focus-within:ring-1",
|
|
||||||
"focus-within:ring-blue-500",
|
|
||||||
);
|
|
||||||
|
|
||||||
this._blurTimeout = setTimeout(() => {
|
this._blurTimeout = setTimeout(() => {
|
||||||
if (
|
if (!this.contains(document.activeElement)) this._hideOptionsList();
|
||||||
!this.contains(document.activeElement) ||
|
|
||||||
document.activeElement === this.createNewButton
|
|
||||||
) {
|
|
||||||
if (
|
|
||||||
document.activeElement !== this.createNewButton &&
|
|
||||||
!(this.optionsListElement && this.optionsListElement.contains(document.activeElement))
|
|
||||||
) {
|
|
||||||
this._hideOptionsList();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, 150);
|
}, 150);
|
||||||
}
|
}
|
||||||
|
|
||||||
_handleOptionMouseDown(event) {
|
_handleOptionMouseDown(event) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
}
|
}
|
||||||
|
|
||||||
_handleOptionClick(event) {
|
_handleOptionClick(event) {
|
||||||
const li = event.target.closest("li[data-id]");
|
const li = event.target.closest("li[data-id]");
|
||||||
if (li) {
|
if (li && li.dataset.id) this._selectItem(li.dataset.id);
|
||||||
const itemId = li.dataset.id;
|
|
||||||
this._selectItem(itemId);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
_selectItem(itemId) {
|
_selectItem(itemId) {
|
||||||
if (itemId && !this._value.includes(itemId)) {
|
if (itemId && !this._value.includes(itemId)) this.value = [...this._value, itemId];
|
||||||
this._value.push(itemId);
|
|
||||||
this._updateFormValue();
|
|
||||||
this._renderSelectedItems();
|
|
||||||
}
|
|
||||||
if (this.inputElement) this.inputElement.value = "";
|
if (this.inputElement) this.inputElement.value = "";
|
||||||
this._filteredOptions = [];
|
this._filteredOptions = [];
|
||||||
this._hideOptionsList();
|
this._hideOptionsList();
|
||||||
// Create button remains visible
|
if (this.inputElement && !this.hasAttribute("disabled")) this.inputElement.focus();
|
||||||
if (this.inputElement) this.inputElement.focus();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
_handleDeleteSelectedItem(itemId) {
|
_handleDeleteSelectedItem(itemId) {
|
||||||
this._value = this._value.filter((id) => id !== itemId);
|
this.value = this._value.filter((id) => id !== itemId);
|
||||||
this._updateFormValue();
|
if (this.inputElement && this.inputElement.value)
|
||||||
this._renderSelectedItems();
|
|
||||||
if (this.inputElement) {
|
|
||||||
this.inputElement.focus();
|
|
||||||
this._handleInput({ target: this.inputElement });
|
this._handleInput({ target: this.inputElement });
|
||||||
}
|
if (this.inputElement && !this.hasAttribute("disabled")) this.inputElement.focus();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
customElements.define("multi-select-simple", MultiSelectSimple);
|
|
||||||
|
|
||||||
// --- Demo Page Script for MultiSelectSimple ---
|
|
||||||
const myFormSimple = document.getElementById("myFormSimple");
|
|
||||||
const formDataDisplaySimple = document.getElementById("form-data-display-simple");
|
|
||||||
const formDataPreSimple = formDataDisplaySimple.querySelector("pre");
|
|
||||||
|
|
||||||
myFormSimple.addEventListener("submit", function (event) {
|
|
||||||
event.preventDefault();
|
|
||||||
const formData = new FormData(this);
|
|
||||||
let output = "";
|
|
||||||
console.log("Simple Form Submitted. FormData entries:");
|
|
||||||
for (let [name, value] of formData.entries()) {
|
|
||||||
console.log(`FormData - ${name}: ${value}`);
|
|
||||||
output += `FormData - ${name}: ${value}\n`;
|
|
||||||
}
|
|
||||||
const simpleItemSelectEl = document.getElementById("simple-item-select");
|
|
||||||
output += `\nCustom element .value (IDs): [${simpleItemSelectEl.value.join(", ")}]`;
|
|
||||||
|
|
||||||
formDataPreSimple.textContent = output;
|
|
||||||
formDataDisplaySimple.classList.remove("hidden");
|
|
||||||
});
|
|
||||||
|
|
||||||
myFormSimple.addEventListener("reset", function (event) {
|
|
||||||
console.log("Simple Form reset!");
|
|
||||||
formDataPreSimple.textContent = "";
|
|
||||||
formDataDisplaySimple.classList.add("hidden");
|
|
||||||
setTimeout(() => {
|
|
||||||
const el = document.getElementById("simple-item-select");
|
|
||||||
if (el) {
|
|
||||||
console.log(`simple-item-select .value after reset:`, el.value);
|
|
||||||
if (el.hiddenSelect) {
|
|
||||||
const hiddenOptions = Array.from(el.hiddenSelect.options).map((opt) => opt.value);
|
|
||||||
console.log(`simple-item-select hidden select values:`, hiddenOptions);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, 0);
|
|
||||||
});
|
|
||||||
|
|||||||
@@ -575,4 +575,240 @@
|
|||||||
transform: rotate(360deg);
|
transform: rotate(360deg);
|
||||||
} /* Pause at the final position */
|
} /* Pause at the final position */
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Multi-Select-Role example styles */
|
||||||
|
.msr-selected-items-container {
|
||||||
|
@apply rounded-md;
|
||||||
|
}
|
||||||
|
.msr-placeholder-no-selection-text {
|
||||||
|
@apply text-sm text-gray-500 italic px-2 py-1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.msr-input-area-wrapper {
|
||||||
|
@apply p-2 rounded-md;
|
||||||
|
}
|
||||||
|
.msr-input-area-wrapper.msr-input-area-default-border {
|
||||||
|
@apply border border-gray-300;
|
||||||
|
}
|
||||||
|
.msr-input-area-wrapper.msr-input-area-default-border:focus-within {
|
||||||
|
@apply focus-within:border-gray-500 focus-within:ring-1 focus-within:ring-gray-400;
|
||||||
|
}
|
||||||
|
.msr-input-area-wrapper.msr-input-area-staged {
|
||||||
|
@apply border border-transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.msr-text-input {
|
||||||
|
@apply bg-transparent text-sm placeholder-gray-400;
|
||||||
|
}
|
||||||
|
|
||||||
|
.msr-selected-item-pill {
|
||||||
|
@apply bg-gray-200 text-gray-700 px-3 py-[0.3rem] rounded-md text-sm inline-flex items-center m-0.5;
|
||||||
|
}
|
||||||
|
.msr-item-name {
|
||||||
|
@apply font-medium;
|
||||||
|
}
|
||||||
|
.msr-item-additional-data {
|
||||||
|
@apply text-xs ml-1 text-gray-600;
|
||||||
|
}
|
||||||
|
.msr-selected-item-role {
|
||||||
|
@apply font-semibold text-xs ml-1 text-gray-800;
|
||||||
|
}
|
||||||
|
.msr-selected-item-delete-btn {
|
||||||
|
@apply bg-transparent border-none text-gray-500 text-lg leading-none px-1 cursor-pointer opacity-60 transition-opacity duration-200;
|
||||||
|
}
|
||||||
|
.msr-selected-item-delete-btn:hover {
|
||||||
|
@apply hover:opacity-100 hover:text-gray-900;
|
||||||
|
}
|
||||||
|
|
||||||
|
.msr-staged-item-pill {
|
||||||
|
@apply bg-gray-100 text-gray-800 px-2 py-1 rounded-md text-sm font-medium;
|
||||||
|
}
|
||||||
|
.msr-staged-item-text {
|
||||||
|
@apply mr-2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.msr-staged-role-select {
|
||||||
|
@apply px-2 py-1 text-sm rounded-md border border-gray-300 bg-white outline-none text-gray-700;
|
||||||
|
}
|
||||||
|
.msr-staged-role-select:focus {
|
||||||
|
@apply focus:border-gray-500 focus:ring-1 focus:ring-gray-400;
|
||||||
|
}
|
||||||
|
|
||||||
|
.msr-staged-cancel-btn {
|
||||||
|
@apply w-5 h-5 bg-gray-200 text-gray-600 rounded-full text-sm leading-none cursor-pointer;
|
||||||
|
}
|
||||||
|
.msr-staged-cancel-btn:hover {
|
||||||
|
@apply hover:bg-gray-300 hover:text-gray-800;
|
||||||
|
}
|
||||||
|
|
||||||
|
.msr-pre-add-button {
|
||||||
|
@apply w-10 h-[42px] text-xl rounded-md bg-gray-50 text-gray-700 border border-gray-300 font-semibold outline-none;
|
||||||
|
}
|
||||||
|
.msr-pre-add-button:focus {
|
||||||
|
@apply focus:border-gray-500 focus:ring-1 focus:ring-gray-400;
|
||||||
|
}
|
||||||
|
.msr-pre-add-button:hover {
|
||||||
|
@apply hover:bg-gray-100;
|
||||||
|
}
|
||||||
|
.msr-pre-add-button:disabled {
|
||||||
|
@apply disabled:bg-gray-200 disabled:text-gray-400 disabled:cursor-not-allowed disabled:border-gray-200;
|
||||||
|
}
|
||||||
|
.msr-pre-add-button.hidden {
|
||||||
|
@apply hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.msr-add-button {
|
||||||
|
@apply px-4 py-2 text-sm rounded-md bg-gray-600 text-white font-medium;
|
||||||
|
}
|
||||||
|
.msr-add-button:hover {
|
||||||
|
@apply hover:bg-gray-700;
|
||||||
|
}
|
||||||
|
.msr-add-button:disabled {
|
||||||
|
@apply disabled:bg-gray-300 disabled:cursor-not-allowed;
|
||||||
|
}
|
||||||
|
.msr-add-button.hidden {
|
||||||
|
@apply hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.msr-options-list {
|
||||||
|
@apply bg-white border border-gray-300 rounded-md shadow-md;
|
||||||
|
}
|
||||||
|
.msr-options-list.hidden {
|
||||||
|
@apply hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.msr-option-item {
|
||||||
|
@apply px-3 py-2 text-sm cursor-pointer transition-colors duration-75;
|
||||||
|
}
|
||||||
|
.msr-option-item:hover {
|
||||||
|
@apply bg-gray-100 text-gray-800;
|
||||||
|
}
|
||||||
|
.msr-option-item-highlighted {
|
||||||
|
@apply bg-gray-100 text-gray-800;
|
||||||
|
}
|
||||||
|
.msr-option-item-name {
|
||||||
|
@apply font-medium;
|
||||||
|
}
|
||||||
|
.msr-option-item-detail {
|
||||||
|
@apply text-xs ml-2 text-gray-500;
|
||||||
|
}
|
||||||
|
.msr-option-item-highlighted .msr-option-item-detail,
|
||||||
|
.msr-option-item:hover .msr-option-item-detail {
|
||||||
|
/* Ensure detail text color changes on hover too */
|
||||||
|
@apply text-gray-600;
|
||||||
|
}
|
||||||
|
|
||||||
|
multi-select-role[disabled] {
|
||||||
|
/* This remains standard CSS as Tailwind's disabled: variant is for native elements */
|
||||||
|
opacity: 0.6;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.msr-hidden-select {
|
||||||
|
/* No specific styling needed as it's visually hidden by JS/inline style */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* --- MultiSelectSimple Component Base Styles (using @apply) --- */
|
||||||
|
.mss-component-wrapper {
|
||||||
|
/* 'relative' is set inline for positioning dropdown */
|
||||||
|
}
|
||||||
|
|
||||||
|
.mss-selected-items-container {
|
||||||
|
@apply border border-gray-300 p-1.5 rounded;
|
||||||
|
/* Tailwind classes from component: flex flex-wrap gap-1 mb-1 min-h-[38px] */
|
||||||
|
}
|
||||||
|
|
||||||
|
.mss-no-items-text {
|
||||||
|
@apply italic text-xs text-gray-500 p-1 w-full; /* Adjusted font size slightly to match 'xs' */
|
||||||
|
}
|
||||||
|
|
||||||
|
.mss-selected-item-pill {
|
||||||
|
@apply bg-gray-200 text-gray-800 py-0.5 px-2 rounded text-xs leading-5; /* Adjusted font size and padding */
|
||||||
|
/* Tailwind classes from component: flex items-center */
|
||||||
|
}
|
||||||
|
|
||||||
|
.mss-selected-item-text {
|
||||||
|
/* Base styles for text part of the pill */
|
||||||
|
}
|
||||||
|
.mss-selected-item-pill-detail {
|
||||||
|
@apply ml-1 opacity-75 text-xs text-gray-600;
|
||||||
|
}
|
||||||
|
.mss-selected-item-pill-detail.hidden {
|
||||||
|
@apply hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mss-selected-item-delete-btn {
|
||||||
|
@apply bg-transparent border-none text-gray-600 opacity-70 cursor-pointer ml-1 text-base leading-none align-middle hover:opacity-100 hover:text-gray-900 disabled:opacity-40 disabled:cursor-not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mss-input-controls-container {
|
||||||
|
/* Tailwind classes from component: flex items-center space-x-2 */
|
||||||
|
}
|
||||||
|
|
||||||
|
.mss-input-wrapper {
|
||||||
|
@apply border border-gray-300 rounded;
|
||||||
|
/* Tailwind classes from component: relative flex items-center flex-grow */
|
||||||
|
}
|
||||||
|
.mss-input-wrapper-focused {
|
||||||
|
@apply border-indigo-600 ring-1 ring-indigo-600; /* Using ring for focus shadow */
|
||||||
|
}
|
||||||
|
|
||||||
|
.mss-text-input {
|
||||||
|
@apply py-1.5 px-2 text-sm;
|
||||||
|
/* Tailwind classes from component: w-full outline-none bg-transparent */
|
||||||
|
}
|
||||||
|
.mss-text-input::placeholder {
|
||||||
|
@apply text-gray-400 italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mss-create-new-button {
|
||||||
|
@apply bg-gray-100 text-gray-700 border border-gray-300 py-1 px-1.5 text-sm rounded hover:bg-gray-200 hover:border-gray-400 disabled:bg-gray-50 disabled:text-gray-400 disabled:border-gray-200 disabled:opacity-70 disabled:cursor-not-allowed;
|
||||||
|
}
|
||||||
|
.mss-create-new-button.hidden {
|
||||||
|
@apply hidden !important; /* Ensure it hides */
|
||||||
|
}
|
||||||
|
|
||||||
|
.mss-options-list {
|
||||||
|
@apply bg-white border border-gray-300 rounded shadow-md; /* Using shadow-md as a softer default */
|
||||||
|
/* Tailwind classes from component: absolute z-20 w-full max-h-60 overflow-y-auto mt-1 hidden */
|
||||||
|
}
|
||||||
|
|
||||||
|
.mss-option-item {
|
||||||
|
@apply text-gray-700 py-1.5 px-2.5 text-sm cursor-pointer transition-colors duration-75 hover:bg-gray-100;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mss-option-item-name {
|
||||||
|
@apply font-medium;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mss-option-item-detail {
|
||||||
|
@apply text-gray-500 text-xs ml-1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mss-option-item-highlighted {
|
||||||
|
@apply bg-indigo-100 text-indigo-800;
|
||||||
|
}
|
||||||
|
.mss-option-item-highlighted .mss-option-item-name {
|
||||||
|
/* @apply font-medium; */ /* Already set by .mss-option-item-name, inherit color from parent */
|
||||||
|
}
|
||||||
|
.mss-option-item-highlighted .mss-option-item-detail {
|
||||||
|
@apply text-indigo-700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mss-hidden-select {
|
||||||
|
/* Styles are inline in _render for !important, no change needed here */
|
||||||
|
}
|
||||||
|
|
||||||
|
multi-select-simple[disabled] {
|
||||||
|
@apply opacity-60; /* Adjusted opacity */
|
||||||
|
}
|
||||||
|
multi-select-simple[disabled] .mss-selected-items-container {
|
||||||
|
@apply bg-gray-100;
|
||||||
|
}
|
||||||
|
multi-select-simple[disabled] .mss-selected-item-pill {
|
||||||
|
@apply bg-gray-300 text-gray-500;
|
||||||
|
}
|
||||||
|
multi-select-simple[disabled] .mss-selected-item-delete-btn {
|
||||||
|
@apply text-gray-400;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user