Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

TH-321: Origins > Shared > Create the dropdown select component #171

Merged
merged 14 commits into from
Jan 23, 2025
Merged
133 changes: 133 additions & 0 deletions themes/origins/assets/component-select.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
yc-select{
--select-bg-color:var(--section-bg-color, var(--color-base-white));
--select-text-color:rgb(var(--section-text-color, var(--color-base-black)));
width:100%;
display:flex;
gap:var(--gap-md);
align-items:center;
position:relative;
}
yc-select yc-select-trigger.yc-btn.icon{
width:100%;
cursor:pointer;
-webkit-user-select:none;
-moz-user-select:none;
user-select:none;
font-size:var(--text-sm);
padding:8px 12px;
justify-content:space-between;
}
yc-select yc-select-trigger.yc-btn.icon yc-select-value{
width:90%;
overflow:hidden;
white-space:nowrap;
text-overflow:ellipsis;
}
yc-select yc-select-trigger.yc-btn.icon::after{
content:"";
width:6px;
height:6px;
rotate:45deg;
position:relative;
border-radius:1px;
inset-block-start:-2px;
border-block-end:1.5px solid currentColor;
border-inline-end:1.5px solid currentColor;
transition:var(--transition-duration-normal);
}
yc-select yc-select-content{
display:grid;
z-index:2;
width:100%;
position:absolute;
inset-block-start:calc(100% + 0.5em);
border-radius:var(--radius-md);
background-color:var(--select-bg-color);
transition:var(--transition-duration-normal);
border:1px solid color-mix(in srgb, currentColor 20%, transparent);
}
yc-select yc-select-content:not([data-visible=true]){
opacity:0;
filter:blur(2px);
visibility:hidden;
translate:0 -0.5em;
}
yc-select yc-select-content:not([data-visible=true])[is-above]{
translate:0 0.5em;
}
yc-select yc-select-content[is-above]{
inset-block-start:auto;
inset-block-end:calc(100% + 0.5em);
}
yc-select yc-select-content input[type=search]{
z-index:2;
position:sticky;
inset-block-start:0;
margin-block-end:4px;
padding:10px 12px;
color:var(--select-text-color);
background-color:var(--select-bg-color);
border-block-end:1px solid color-mix(in srgb, currentColor 8%, transparent) !important;
}
yc-select yc-select-content label{
padding:8px 12px;
position:relative;
margin-inline:4px;
font-size:var(--text-sm);
border-radius:var(--radius-md);
transition:var(--transition-duration-normal);
}
yc-select yc-select-content label span{
overflow:hidden;
display:-webkit-box;
-webkit-line-clamp:1;
text-overflow:ellipsis;
-webkit-box-orient:vertical;
}
yc-select yc-select-content label:first-child{
margin-block-start:4px;
}
yc-select yc-select-content label:last-child{
margin-block-end:4px;
}
yc-select yc-select-content label::after{
width:10px;
height:5px;
rotate:315deg;
position:absolute;
translate:-50% -50%;
inset-inline-end:4px;
inset-block-start:50%;
border:solid currentColor;
border-width:0 0 1.5px 1.5px;
}
[dir="rtl"] yc-select yc-select-content label::after{
inset-inline-end:16px;
}
yc-select yc-select-content label:has(input:checked)::after{
content:"";
}
yc-select yc-select-content label:has(input:disabled){
opacity:0.5;
pointer-events:none;
}
yc-select yc-select-content label:hover{
background-color:color-mix(in srgb, currentColor 5%, transparent);
}
yc-select yc-select-content:has(label[hidden]){
padding-block-end:4px;
}
yc-select yc-select-content:has(label:nth-of-type(n + 6)){
max-height:260px;
overflow-y:scroll;
}
yc-select yc-select-content[data-no-results]:not([data-no-results=""])::after{
content:attr(data-no-results);
padding:3em 4px;
text-align:center;
font-size:var(--text-sm);
}
yc-select:has(yc-select-content[data-visible=true]) yc-select-trigger::after{
rotate:-135deg;
inset-block-start:2px;
}
110 changes: 110 additions & 0 deletions themes/origins/assets/component-select.js
ibrilBadreddine marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
if (!customElements.get("yc-select")) {
class Select extends HTMLElement {
static observedAttributes = ["name"];

constructor() {
super();
this.state = false;
this.trigger = this.querySelector("yc-select-trigger");
this.placeholder = this.trigger.querySelector("yc-select-value");
this.content = this.querySelector("yc-select-content");
this.search = this.querySelector("yc-select-search");
}

connectedCallback() {
this.setup();
this.listeners();
this.search && this.enableSearch();
}

setup() {
const items = this.content.querySelectorAll("yc-select-item");

items.forEach((item) => {
const { value } = item.attributes;
const label = item.textContent.trim();
const disabled = item.hasAttribute("disabled");
item.outerHTML = `
<label>
<span>${label}</span>
<input type="radio" name="${this.getAttribute("name")}" value="${value?.value}"
${disabled ? "disabled" : ""} hidden>
</label>`;
});
}

listeners() {
this.onSelect();
this.onTrigger();
this.onClickOutSide();
}

enableSearch() {
const placeholder = this.search.getAttribute("placeholder");
const noResultsMsg = this.search.getAttribute("no-results");
const searchInput = document.createElement("input");
searchInput.type = "search";
searchInput.name = "search";
searchInput.placeholder = placeholder;

this.search.replaceWith(searchInput);
this.search = searchInput;

const options = [...this.content.querySelectorAll("label")];
this.onSearch(options, noResultsMsg);
}

toggleState(visible) {
this.state = visible;
this.content.dataset.visible = visible;
}

onSelect() {
const options = this.content.querySelectorAll("label");

options.forEach((opt) =>
opt.addEventListener("change", () => {
this.placeholder.textContent = opt.textContent.trim();
this.toggleState(false);
}),
);
}

onTrigger() {
this.trigger.addEventListener("click", () => {
this.content.toggleAttribute(
"is-above",
innerHeight - this.trigger.getBoundingClientRect().bottom <
this.content.offsetHeight,
);
this.toggleState(!this.state);
});
}

onClickOutSide() {
document.addEventListener("click", (e) => {
const isTrigger = e.composedPath().includes(this.trigger);
const isSearch = e.target.tagName === "INPUT";

if (!isTrigger && !isSearch) this.toggleState(false);
});
}

onSearch(options, no_results_message) {
this.search.addEventListener("input", (e) => {
const query = e.target.value.toLowerCase();
let hasMatch = false;

options.forEach((opt) => {
const isMatch = opt.textContent.toLowerCase().includes(query);
opt.hidden = !isMatch;
hasMatch ||= isMatch;
});

this.content.dataset.noResults = hasMatch ? "" : no_results_message;
});
}
}

customElements.define("yc-select", Select);
}
71 changes: 71 additions & 0 deletions themes/origins/assets/component-upload.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
if (!customElements.get("yc-upload")) {
class Upload extends HTMLElement {
static observedAttributes = ["name"];

constructor() {
super();
this.input = this.querySelector("input[type='file']");
this.info = this.querySelector("[data-info]");
this.unset = this.info.querySelector("[data-unset]");
}

connectedCallback() {
this._render();
}

_render() {
this.onUpload();
}

onUpload() {
this.input.addEventListener("change", () => {
const file = this.input.files?.[0];
if (!file) return;

const reader = new FileReader();

reader.onload = () => {
const base64 = reader.result;
const preview = this.info.querySelector("[data-preview]");

!preview
? this.create(file, base64)
: this.update(file, base64, preview);
this.unset.addEventListener("click", () => this.remove());
};

reader.readAsDataURL(file);
});
}

toKB(size) {
return `${Math.ceil(size / 1024)} KB`;
}

create(file, image_src) {
this.info.insertAdjacentHTML(
"afterbegin",
`<div class="preview" data-preview>
<img src="${image_src}" width="50" height="50" />
<div class="info">
<span class="name">${file.name}</span>
<span class="size">${this.toKB(file.size)}</span>
</div>
</div>`,
);
}

update(file, image_src, element) {
element.querySelector("img").src = image_src;
element.querySelector(".name").textContent = file.name;
element.querySelector(".size").textContent = this.toKB(file.size);
}

remove() {
this.info.querySelector("[data-preview]")?.remove();
this.input.value = null;
}
}

customElements.define("yc-upload", Upload);
}
Loading