@@ -136,13 +114,13 @@
({state.value.selected.length})
0}>
-
+
0 || state.value.query.length > 0}>
-
+
-
+
@@ -151,23 +129,14 @@
-
+
-
+
-
+
-
+
diff --git a/packages/temple-ui-src/src/field/taglist.tml b/packages/temple-ui-src/src/field/taglist.tml
index a575606..b3b9bc8 100644
--- a/packages/temple-ui-src/src/field/taglist.tml
+++ b/packages/temple-ui-src/src/field/taglist.tml
@@ -1,34 +1,77 @@
-
+
-
+
\ No newline at end of file
diff --git a/packages/temple-ui-src/src/field/textlist.tml b/packages/temple-ui-src/src/field/textlist.tml
new file mode 100644
index 0000000..546462f
--- /dev/null
+++ b/packages/temple-ui-src/src/field/textlist.tml
@@ -0,0 +1,107 @@
+
+
+
+ {rows.map(row => row.field)}
+
+
+ {rows.map(row => row.fieldset)}
+
+
\ No newline at end of file
diff --git a/packages/temple-ui-src/src/field/wysiwyg.tml b/packages/temple-ui-src/src/field/wysiwyg.tml
index e69de29..9c13df8 100644
--- a/packages/temple-ui-src/src/field/wysiwyg.tml
+++ b/packages/temple-ui-src/src/field/wysiwyg.tml
@@ -0,0 +1,983 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/packages/temple-ui-src/src/form/button.tml b/packages/temple-ui-src/src/form/button.tml
index 1c9ddd2..ccfd3c5 100644
--- a/packages/temple-ui-src/src/form/button.tml
+++ b/packages/temple-ui-src/src/form/button.tml
@@ -1,3 +1,21 @@
+
+
+ {rows.map(row => row.slot)}
+
+
+ {rows.map(row => row.fieldset)}
+
+
\ No newline at end of file
diff --git a/packages/temple-ui-src/src/utilities/fieldset.ts b/packages/temple-ui-src/src/utilities/fieldset.ts
new file mode 100644
index 0000000..a34ee02
--- /dev/null
+++ b/packages/temple-ui-src/src/utilities/fieldset.ts
@@ -0,0 +1,157 @@
+import type { MouseEvent } from '@ossph/temple/dist/types';
+import type TempleComponent from '@ossph/temple/dist/client/TempleComponent';
+import type StyleSet from '@ossph/temple/dist/style/StyleSet';
+
+import TempleRegistry from '@ossph/temple/dist/client/TempleRegistry';
+import setColor from './style/color';
+import setCurve from './style/curve';
+import setPadding from './style/padding';
+
+export function buttonStyles(props: Record
, styles: StyleSet) {
+ const {
+ //font size
+ size, xs, sm, md, lg,
+ xl, xl2, xl3, xl4, xl5,
+ //layouts
+ outline, solid, transparent,
+ //others
+ full
+ } = props;
+
+ //determine padding
+ const pad = setPadding(props, styles, 'button');
+ if (!pad) {
+ //determine padding by size
+ xs ? styles.add('button', 'padding', '2px 4px')
+ : sm ? styles.add('button', 'padding', '5px 10px')
+ : md ? styles.add('button', 'padding', '8px 16px')
+ : lg ? styles.add('button', 'padding', '12px 24px')
+ : xl ? styles.add('button', 'padding', '15px 30px')
+ : xl2 ? styles.add('button', 'padding', '18px 36px')
+ : xl3 ? styles.add('button', 'padding', '22px 44px')
+ : xl4 ? styles.add('button', 'padding', '26px 52px')
+ : xl5 ? styles.add('button', 'padding', '30px 60px')
+ : size ? styles.add('button', 'padding', size)
+ : styles.add('button', 'padding', '5px 10px');
+ }
+
+ //determine curve
+ setCurve(props, styles, false, 'button');
+ //determine width
+ if (full) {
+ styles.add('button', 'width', '100%');
+ }
+ //if outline or transparent
+ if (outline || transparent) {
+ setColor(props, styles, 'var(--muted)', 'button', 'color');
+ setColor(props, styles, 'var(--muted)', 'button', 'border-color');
+ styles.add('button', 'border-style', 'solid');
+ styles.add('button', 'border-width', '1px');
+ if (outline) {
+ styles.add('button', 'background-color', 'var(--white)');
+ }
+ //it's solid
+ } else if (solid) {
+ styles.add('button', 'border', '0');
+ styles.add('button', 'color', 'var(--white)');
+ setColor(props, styles, 'var(--muted)', 'button', 'background-color');
+ } else {
+ styles.add('button', 'border', '0');
+ styles.add('button', 'color', 'var(--white)');
+ setColor(props, styles, 'var(--muted)', 'button', 'background-color');
+ }
+}
+
+export function getHandlers(host: TempleComponent, template: Node[]) {
+ const { name, legend } = host.props;
+ const handlers = {
+ add: (e: MouseEvent) => {
+ const shadow = host.shadowRoot;
+ if (!shadow) return;
+ const button = shadow.querySelector('button');
+ if (!button) return;
+ const { fieldset, slot } = handlers.create(host.childElementCount);
+ button.before(fieldset);
+ host.appendChild(slot);
+ },
+ create: (index: number, valueset: Record = {}) => {
+ const fields = handlers.clone(index, valueset);
+ const slot = TempleRegistry.createElement(
+ 'div', { slot: `row-${index}`}, fields
+ ).element as HTMLElement;
+ const title = legend ? legend.replace('%s', index + 1): undefined;
+ const remove = TempleRegistry.createElement(
+ 'a', {}, [ '×' ]
+ ).element as HTMLElement;
+ const label = legend
+ ? TempleRegistry.createElement('span', {}, [ title ]).element
+ : undefined;
+ const fieldset = TempleRegistry.createElement('fieldset', {}, [
+ TempleRegistry.createElement('legend', {}, [ label, remove ]).element,
+ TempleRegistry.createElement('slot', { name: `row-${index}` }).element
+ ]).element as HTMLElement;
+ remove.addEventListener('click', () => handlers.remove(fieldset, slot));
+ return { fieldset, slot };
+ },
+ clone: (index: number, valueset: Record = {}) => {
+ return template.map(element => {
+ if (element instanceof HTMLElement) {
+ const field = TempleRegistry.cloneElement(element, true).element;
+ //find names
+ const key = field.getAttribute('name');
+ if (name && key) {
+ field.setAttribute('data-key', key);
+ field.setAttribute('name', `${name}[${index}][${key}]`);
+ }
+ if (key && typeof valueset[key] !== 'undefined') {
+ //@ts-ignore
+ field.value = valueset[key];
+ field.setAttribute('value', valueset[key]);
+ }
+ Array.from(field.querySelectorAll('[name]')).forEach(element => {
+ const key = element.getAttribute('name');
+ if (name && key) {
+ element.setAttribute('data-key', key);
+ element.setAttribute('name', `${name}[${index}][${key}]`);
+ }
+ if (key && typeof valueset[key] !== 'undefined') {
+ //@ts-ignore
+ element.value = valueset[key];
+ element.setAttribute('value', valueset[key]);
+ }
+ });
+ return field;
+ }
+ return element.cloneNode();
+ });
+ },
+ remove: (fieldset: HTMLElement, slot: HTMLElement) => {
+ const shadow = host.shadowRoot;
+ if (!shadow) return;
+ fieldset.remove();
+ slot.remove();
+ shadow.querySelectorAll('fieldset slot').forEach((slot, index) => {
+ slot.setAttribute('name', `row-${index}`);
+ });
+ host.querySelectorAll(':scope > div[slot]').forEach((slot, index) => {
+ slot.setAttribute('slot', `row-${index}`);
+ if (name) {
+ Array.from(slot.querySelectorAll('[name]')).forEach(element => {
+ const key = element.getAttribute('data-key');
+ if (key) {
+ element.setAttribute('name', `${name}[${index}][${key}]`);
+ }
+ });
+ }
+ });
+ if (legend) {
+ host.shadowRoot?.querySelectorAll(
+ 'fieldset legend span'
+ ).forEach((span, index) => {
+ span.textContent = legend.replace('%s', index + 1);
+ });
+ }
+ }
+ };
+ return handlers;
+}
\ No newline at end of file
diff --git a/packages/temple-ui-src/src/utilities/input.ts b/packages/temple-ui-src/src/utilities/input.ts
index 3c0830c..fc93af6 100644
--- a/packages/temple-ui-src/src/utilities/input.ts
+++ b/packages/temple-ui-src/src/utilities/input.ts
@@ -62,38 +62,10 @@ export function getProps(host: TempleField) {
};
};
-export function getHandlers(
- host: TempleField,
- change?: Function,
- update?: Function
+export function setDefaultStyles(
+ props: Record,
+ styles: StyleSet
) {
- //handlers
- const handlers = {
- change(e: ChangeEvent) {
- change && change(e);
- update && update(e.target.value);
- },
- attribute(e: AttributeChangeEvent) {
- //accepts: error,accept,autocomplete,checked,disabled,max,min,
- // multiple,name,pattern,readonly,required,step,value
- const { action, name, value, target } = e.detail;
- const input = target.querySelector('input');
- switch (action) {
- case 'add':
- case 'update':
- input?.setAttribute(name, value);
- break;
- case 'remove':
- input?.removeAttribute(name);
- break;
- }
- }
- };
- host.on('attributechange', handlers.attribute);
- return handlers;
-};
-
-export function setDefaultStyles(props: Record, styles: StyleSet) {
const { background, border, error } = props;
//determine display
setDisplay(props, styles, 'inline-block', ':host');
@@ -117,6 +89,7 @@ export function setDefaultStyles(props: Record, styles: StyleSet) {
styles.add('::slotted(*)', 'background', 'transparent');
styles.add('::slotted(*)', 'border', '0');
styles.add('::slotted(*)', 'box-sizing', 'border-box');
+ styles.add('::slotted(*)', 'font-family', 'inherit');
styles.add('::slotted(*)', 'display', 'block');
styles.add('::slotted(*)', 'height', '100%');
styles.add('::slotted(*:focus)', 'outline', 'none');
@@ -126,7 +99,7 @@ export function setDefaultStyles(props: Record, styles: StyleSet) {
//determine align
setAlign(props, styles, 'left', '::slotted(*)');
//determine font size
- setSize(props, styles, '13px', '::slotted(*)', 'font-size');
+ setSize(props, styles, 'inherit', '::slotted(*)', 'font-size');
//determine font color
setColor(props, styles, 'var(--black)', '::slotted(*)', 'color');
//determine padding
@@ -134,4 +107,35 @@ export function setDefaultStyles(props: Record, styles: StyleSet) {
if (!padding) {
styles.add('::slotted(*)', 'padding', '7px');
}
+};
+
+export function getHandlers(
+ host: TempleField,
+ change?: Function,
+ update?: Function
+) {
+ //handlers
+ const handlers = {
+ change(e: ChangeEvent) {
+ change && change(e);
+ update && update(e.target.value);
+ },
+ attribute(e: AttributeChangeEvent) {
+ //accepts: error,accept,autocomplete,checked,disabled,max,min,
+ // multiple,name,pattern,readonly,required,step,value
+ const { action, name, value, target } = e.detail;
+ const input = target.querySelector('input');
+ switch (action) {
+ case 'add':
+ case 'update':
+ input?.setAttribute(name, value);
+ break;
+ case 'remove':
+ input?.removeAttribute(name);
+ break;
+ }
+ }
+ };
+ host.on('attributechange', handlers.attribute);
+ return handlers;
};
\ No newline at end of file
diff --git a/packages/temple-ui-src/src/utilities/select.ts b/packages/temple-ui-src/src/utilities/select.ts
index 6bf56e8..0992283 100644
--- a/packages/temple-ui-src/src/utilities/select.ts
+++ b/packages/temple-ui-src/src/utilities/select.ts
@@ -1,6 +1,9 @@
-import type { MouseEvent, KeyboardEvent } from '@ossph/temple/dist/types';
+import type {
+ MouseEvent,
+ KeyboardEvent,
+ AttributeChangeEvent
+} from '@ossph/temple/dist/types';
import type TempleComponent from '@ossph/temple/dist/client/TempleComponent';
-import type TempleField from '@ossph/temple/dist/client/TempleField';
import TempleRegistry from '@ossph/temple/dist/client/TempleRegistry';
import signal from '@ossph/temple/dist/client/signal';
@@ -13,9 +16,50 @@ export type State = {
selected: Node[]
};
+export function makeOptions(children: Node[]) {
+ return Array.from(children)
+ //only HTML elements
+ .filter(option => option instanceof HTMLElement)
+ //filter out hidden inputs
+ .filter(option => option.nodeName !== 'INPUT'
+ || !option.hasAttribute('type')
+ || option.getAttribute('type') !== 'hidden'
+ )
+ //filter out options with slot attributes that are not filtered
+ .filter(option => !option.hasAttribute('slot')
+ || option.getAttribute('slot') === 'filtered'
+ )
+ //build options markup
+ .map(option => {
+ //if not an option, return the option as is
+ if (option.nodeName !== 'OPTION') {
+ return option;
+ }
+ //get the attributes
+ const attributes = TempleRegistry.get(option)?.attributes || {};
+ //set value if not set
+ attributes.value = attributes.value ? attributes.value
+ : option.hasAttribute('value') ? option.getAttribute('value')
+ : option.innerText.trim();
+ //try to set the keyword attribute
+ attributes.keyword = attributes.keyword ? attributes.keyword
+ : option.hasAttribute('keyword') ? option.getAttribute('keyword')
+ : undefined;
+ //set class if not set (this is for shadow styles)
+ attributes['class'] = attributes['class'] ? attributes['class']
+ : option.hasAttribute('class') ? option.getAttribute('class')
+ : 'select-default-option';
+ //get the children
+ const children = Array.from(option.childNodes);
+ //create and return the option wrapper
+ return TempleRegistry.createElement('div', attributes, children).element;
+ });
+}
+
export function getHandlers(
host: TempleComponent,
- options: Element[]
+ options: Element[],
+ slot = true
) {
const {
value, multiple,
@@ -82,8 +126,7 @@ export function getHandlers(
);
//a re-render just happened, so we need
//to re-focus the input to continue typing
- const field = host.shadowRoot?.querySelector('.input') as TempleField|null;
- const input = field?.querySelector('input');
+ const input = host.shadowRoot?.querySelector('.input') as HTMLInputElement|null;
input?.focus();
input?.setSelectionRange(selection[0], selection[1]);
//trigger filter event
@@ -197,7 +240,7 @@ export function getHandlers(
//now query the keyword
return keyword.toLowerCase().includes(query.toLowerCase());
}).map(option => {
- if (option instanceof Element) {
+ if (slot && option instanceof Element) {
option.setAttribute('slot', 'filtered');
}
return option;
@@ -218,13 +261,44 @@ export function getHandlers(
return values.includes(value);
}).map(option => {
const clone = option.cloneNode(true);
- if (clone instanceof Element) {
+ if (slot && clone instanceof Element) {
clone.setAttribute('slot', 'selected');
}
return clone;
});
return { show, query, values, options, filtered, selected };
+ },
+ attribute(e: AttributeChangeEvent) {
+ //accepts: name, value
+ const { action, name, value, target } = e.detail;
+ const inputs = Array.from(
+ target.querySelectorAll(':scope > input[hidden]')
+ );
+ for (let i = 0; i < inputs.length; i++) {
+ const input = inputs[i];
+ if (name === 'name') {
+ switch (action) {
+ case 'add':
+ case 'update':
+ input.setAttribute('name', value);
+ break;
+ case 'remove':
+ input.removeAttribute('name');
+ break;
+ }
+ } else if (name === 'value') {
+ const values = Array.isArray(value) ? value : [ value ];
+ if (action === 'remove'
+ || typeof values[i] === 'undefined'
+ || values[i] === null
+ ) {
+ input.removeAttribute(name);
+ continue;
+ }
+ input.setAttribute(name, values[i]);
+ }
+ }
}
};
@@ -237,6 +311,6 @@ export function getHandlers(
const out = handlers.out as unknown as EventListener;
host.addEventListener('mouseover', over);
host.addEventListener('mouseout', out);
-
+ host.on('attributechange', handlers.attribute);
return { state, local, ...handlers };
};
diff --git a/packages/temple-web/src/pages/ui/form/index.dtml b/packages/temple-web/src/pages/ui/form/index.dtml
index 6682370..304ff52 100644
--- a/packages/temple-web/src/pages/ui/form/index.dtml
+++ b/packages/temple-web/src/pages/ui/form/index.dtml
@@ -6,6 +6,7 @@
+
@@ -18,16 +19,20 @@
+
+
+
+
+
-
-
+