UNPKG

sober

Version:

Sober is a library of UI components designed with reference to Material You.

365 lines (364 loc) 12.1 kB
import { useElement, useProps } from './core/element.js'; const name = 's-text-field'; const props = useProps({ $label: '', $placeholder: '', disabled: false, type: ['text', 'number', 'password', 'multiline'], error: false, $value: '', $maxLength: -1, readOnly: false, multiLine: false, countered: false }); const style = /*css*/ ` :host{ display: inline-grid; vertical-align: middle; font-size: .875rem; min-height: 48px; width: 280px; color: var(--s-color-on-surface, ${"#191C1E" /* Theme.colorOnSurface */}); --text-field-border-radius: 4px; --text-field-border-color: var(--s-color-outline, ${"#70787D" /* Theme.colorOutline */}); --text-field-padding: 16px; --text-field-padding-top: var(--text-field-padding); --text-field-padding-bottom: var(--text-field-padding); --text-field-padding-left: var(--text-field-padding); --text-field-padding-right: var(--text-field-padding); } :host([disabled=true]){ pointer-events: none; opacity: .38; } :host([type=multiline]){ line-height: 24px; --text-field-padding-top: 12px; --text-field-padding-bottom: 12px; } .field{ display: block; min-height: inherit; font-size: inherit; --field-border-radius: var(--text-field-border-radius); --field-border-color: var(--text-field-border-color); --field-padding: var(--text-field-padding); --field-padding-top: var(--text-field-padding-top); --field-padding-bottom: var(--text-field-padding-bottom); --field-padding-left: var(--text-field-padding-left); --field-padding-right: var(--text-field-padding-right); } :host([error=true]) .field{ --s-color-primary: var(--s-color-error, ${"#BA1A1A" /* Theme.colorError */}); --field-border-color: var(--s-color-error, ${"#BA1A1A" /* Theme.colorError */}); --field-border-width: 2px; } :host([type=multiline]) .label{ height: fit-content; padding-top: var(--text-field-padding-top); padding-bottom: var(--text-field-padding-bottom); } .view{ flex-grow: 1; padding: 0; flex-direction: column; position: relative; } input, textarea{ border: none; height: 100%; width: 100%; padding-left: var(--field-padding-left); padding-right: var(--field-padding-right); background: none; outline: none; font-size: inherit; color: inherit; box-sizing: border-box; line-height: inherit; font-family: inherit; caret-color: var(--s-color-primary, ${"#006782" /* Theme.colorPrimary */}); display: block; -moz-appearance: textfield; } textarea{ position: absolute; left: 0; top: 0; width: 100%; height: 100%; resize: none; scrollbar-width: none; display: none; } input::placeholder, textarea::placeholder{ color: var(--text-field-border-color); } input::selection, textarea::selection{ background: var(--s-color-primary, ${"#006782" /* Theme.colorPrimary */}); color: var(--s-color-on-primary, ${"#ffffff" /* Theme.colorOnPrimary */}); } :host([type=multiline]) input, .text>.counter{ display: none; } textarea, .shadow{ line-height: inherit; word-wrap: break-word; word-break: break-all; white-space: pre-wrap; box-sizing: border-box; padding-top: var(--text-field-padding-top); padding-bottom: var(--text-field-padding-bottom); padding-left: var(--text-field-padding-left); padding-right: var(--text-field-padding-right); } :host([type=multiline]) :is(textarea, .shadow), :host([countered=true]) .counter{ display: block; } input::-webkit-outer-spin-button, input::-webkit-inner-spin-button{ -webkit-appearance: none; } input:is(::-ms-clear, ::-ms-reveal){ display: none; } .shadow{ pointer-events: none; display: none; opacity: 0; width: 100%; min-height: 100%; } .shadow::after{ content: ' '; } .text{ display: flex; align-items: flex-end; width: 100%; box-sizing: border-box; font-size: .75rem; color: var(--text-field-border-color); } :host([error=true]) .text{ color: var(--s-color-error, ${"#BA1A1A" /* Theme.colorError */}); } .text>slot[name=text]{ display: block; flex-grow: 1; } .text>.counter, ::slotted([slot=text]){ margin-top: 8px; } .toggle{ flex-direction: column; } .toggle, .toggle>s-ripple{ display: flex; justify-content: center; align-items: center; } .toggle svg{ pointer-events: none; } .toggle>s-ripple{ display: none; } .toggle>.up, .toggle>.down{ height: 16px; width: 24px; border-radius: 4px; margin-right: 8px; margin-left: -4px; } .toggle>.visibility{ width: 40px; height: 40px; border-radius: 50%; margin: 0 4px 0 -4px; } .toggle>.visibility>svg{ display: none; } :host([type=number]) .toggle>:is(.up, .down), :host([type=password]) :is(.toggle>.visibility, .toggle:not(.show-password)>.visibility>.on, .toggle.show-password>.visibility>.off){ display: flex; } svg, ::slotted(svg){ fill: var(--s-color-on-surface-variant, ${"#40484C" /* Theme.colorOnSurfaceVariant */}); height: 24px; width: 24px; flex-shrink: 0; } ::slotted(s-icon-button[slot=start]){ margin-left: 4px; margin-right: calc(var(--text-field-border-radius) - var(--text-field-padding-left) + 4px); } ::slotted(s-icon-button[slot=end]){ margin-right: 4px; margin-left: calc(var(--text-field-border-radius) - var(--text-field-padding-right) + 4px); } ::slotted(:is(s-icon[slot=start], svg[slot=start])){ margin-left: 12px; margin-right: calc(var(--text-field-border-radius) - var(--text-field-padding-left) + 8px); } ::slotted(:is(s-icon[slot=end], svg[slot=end])){ margin-right: 12px; margin-left: calc(var(--text-field-border-radius) - var(--text-field-padding-right) + 8px); } `; const template = /*html*/ ` <s-field class="field" fixed="false"> <div slot="label" class="label"></div> <div class="view"> <div class="shadow"></div> <input type="text" part="input"> <textarea part="textarea"></textarea> </div> <slot slot="start" name="start"></slot> <slot slot="end" name="end"> <div class="toggle" part="toggle"> <s-ripple class="up"> <svg viewBox="0 -960 960 960"> <path d="m280-400 200-200 200 200H280Z"></path> </svg> </s-ripple> <s-ripple class="down"> <svg viewBox="0 -960 960 960"> <path d="M480-360 280-560h400L480-360Z"></path> </svg> </s-ripple> <s-ripple class="visibility"> <svg viewBox="0 -960 960 960" class="on"> <path d="M480-320q75 0 127.5-52.5T660-500q0-75-52.5-127.5T480-680q-75 0-127.5 52.5T300-500q0 75 52.5 127.5T480-320Zm0-72q-45 0-76.5-31.5T372-500q0-45 31.5-76.5T480-608q45 0 76.5 31.5T588-500q0 45-31.5 76.5T480-392Zm0 192q-146 0-266-81.5T40-500q54-137 174-218.5T480-800q146 0 266 81.5T920-500q-54 137-174 218.5T480-200Zm0-300Zm0 220q113 0 207.5-59.5T832-500q-50-101-144.5-160.5T480-720q-113 0-207.5 59.5T128-500q50 101 144.5 160.5T480-280Z"></path> </svg> <svg viewBox="0 -960 960 960" class="off"> <path d="m644-428-58-58q9-47-27-88t-93-32l-58-58q17-8 34.5-12t37.5-4q75 0 127.5 52.5T660-500q0 20-4 37.5T644-428Zm128 126-58-56q38-29 67.5-63.5T832-500q-50-101-143.5-160.5T480-720q-29 0-57 4t-55 12l-62-62q41-17 84-25.5t90-8.5q151 0 269 83.5T920-500q-23 59-60.5 109.5T772-302Zm20 246L624-222q-35 11-70.5 16.5T480-200q-151 0-269-83.5T40-500q21-53 53-98.5t73-81.5L56-792l56-56 736 736-56 56ZM222-624q-29 26-53 57t-41 67q50 101 143.5 160.5T480-280q20 0 39-2.5t39-5.5l-36-38q-11 3-21 4.5t-21 1.5q-75 0-127.5-52.5T300-500q0-11 1.5-21t4.5-21l-84-82Zm319 93Zm-151 75Z"></path> </svg> </s-ripple> </div> </slot> </s-field> <div class="text" part="text"> <slot name="text"></slot> <div class="counter" part="counter"></div> </div> `; export class TextField extends useElement({ style, template, props, setup(shadowRoot) { const field = shadowRoot.querySelector('.field'); const label = shadowRoot.querySelector('.label'); const textAreaShadow = shadowRoot.querySelector('.shadow'); const counter = shadowRoot.querySelector('.counter'); const toggle = shadowRoot.querySelector('.toggle'); const inputs = { input: shadowRoot.querySelector('input'), textarea: shadowRoot.querySelector('textarea') }; const getInput = () => this.type === 'multiline' ? inputs.textarea : inputs.input; const onCounter = () => { if (!this.countered) return; counter.textContent = `${getInput().value.length}/${this.maxLength}`; }; const onChange = () => this.dispatchEvent(new Event('change')); const onFocus = () => { field.fixed = true; field.focused = true; }; const onBlur = () => { field.focused = false; if (getInput().value === '' && !this.error) field.fixed = false; }; inputs.input.oninput = onCounter; inputs.input.onfocus = onFocus; inputs.input.onblur = onBlur; inputs.input.onchange = onChange; inputs.textarea.onfocus = onFocus; inputs.textarea.onblur = onBlur; inputs.textarea.onchange = onChange; inputs.textarea.oninput = () => { textAreaShadow.textContent = inputs.textarea.value; onCounter(); }; const setNumber = (num) => { this.value = `${parseInt(this.value || '0') + num}`; this.dispatchEvent(new Event('input')); this.dispatchEvent(new Event('change')); }; shadowRoot.querySelector('.up').onclick = () => setNumber(1); shadowRoot.querySelector('.down').onclick = () => setNumber(-1); shadowRoot.querySelector('.visibility').onclick = () => { inputs.input.type = toggle.classList.contains('show-password') ? 'password' : 'text'; toggle.classList.toggle('show-password'); }; return { expose: { get native() { return getInput(); } }, label: (value) => label.textContent = value, type: (value, old) => { inputs.input.type = value === 'password' ? (toggle.classList.contains('show-password') ? 'text' : 'password') : value; if (value === 'multiline') { inputs.textarea.value = inputs.input.value; textAreaShadow.textContent = inputs.input.value; } if (old === 'multiline') inputs.input.value = inputs.textarea.value; }, error: (value) => { if (value) return field.fixed = true; if (getInput().value === '') field.fixed = false; }, value: { get: () => getInput().value, set: (value) => { inputs.input.value = value; inputs.textarea.value = value; textAreaShadow.textContent = value; onCounter(); if (!this.error) field.fixed = value !== ''; } }, placeholder: (value) => { inputs.input.placeholder = value; inputs.textarea.placeholder = value; }, readOnly: (value) => { inputs.input.readOnly = value; inputs.textarea.readOnly = value; }, disabled: (value) => { inputs.input.disabled = value; inputs.textarea.disabled = value; }, maxLength: (value) => { inputs.input.maxLength = value; inputs.textarea.maxLength = value; onCounter(); }, multiLine: (value) => this.type = value ? 'multiline' : 'text', countered: onCounter }; } }) { } TextField.define(name);