Skip to content

Commit

Permalink
fix(smart): wip smart past
Browse files Browse the repository at this point in the history
  • Loading branch information
xmlking committed Nov 25, 2024
1 parent 13d3fc5 commit d56473f
Show file tree
Hide file tree
Showing 11 changed files with 399 additions and 73 deletions.
22 changes: 3 additions & 19 deletions apps/console/src/routes/(app)/ai/pastesmart/+page.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,29 +3,13 @@ import { fail } from '@sveltejs/kit';
import { message, superValidate } from 'sveltekit-superforms';
import { zod } from 'sveltekit-superforms/adapters';
import { z } from 'zod';
import { spSchema } from './schema';

const smartpasteSchema = z.object({
firstName: z.string().nullish(),
lastName: z.string().nullish(),
phoneNumber: z.string().nullish(),
line1: z.string().nullish(),
line2: z.string().nullish(),
city: z.string().nullish(),
state: z.string().nullish(),
zip: z.string().nullish(),
country: z.string().nullish(),
});

const log = new Logger('server:ai:ms');

export const load = async () => {
const form = await superValidate(zod(smartpasteSchema));
return { form };
};
const log = new Logger('smart:past:server');

export const actions = {
default: async ({ request }) => {
const form = await superValidate(request, zod(smartpasteSchema));
const form = await superValidate(request, zod(spSchema));
await sleep(1000);

if (!form.valid) return fail(400, { form });
Expand Down
67 changes: 59 additions & 8 deletions apps/console/src/routes/(app)/ai/pastesmart/+page.svelte
Original file line number Diff line number Diff line change
@@ -1,15 +1,17 @@
<script lang="ts">
import { handleMessage } from '$lib/components/layout/toast-manager';
import { SmartPaste } from '@spectacular/smart';
import { SmartPaste, SmartSupport } from '@spectacular/smart';
import { smartPast } from '@spectacular/smart/actions';
import { getLoadingState } from '$lib/stores/loading';
import { getToastStore } from '@skeletonlabs/skeleton';
import { DebugShell } from '@spectacular/skeleton/components';
import { Alerts } from '@spectacular/skeleton/components/form';
import { Logger } from '@spectacular/utils';
import * as Form from 'formsnap';
import { fade } from 'svelte/transition';
import SuperDebug, { superForm } from 'sveltekit-superforms';
import { onMount } from 'svelte';
import SuperDebug, { defaults, superForm } from 'sveltekit-superforms';
import { zod, zodClient } from 'sveltekit-superforms/adapters';
import { spSchema } from './schema.js';
const log = new Logger('ai:smart:browser');
export let data;
Expand All @@ -18,13 +20,15 @@ export let data;
const toastStore = getToastStore();
const loadingState = getLoadingState();
const form = superForm(data.form, {
id: 'ai-form',
// Search form
const form = superForm(defaults(zod(spSchema)), {
id: 'smart-past-form',
dataType: 'json',
taintedMessage: null,
syncFlashMessage: false,
delayMs: 100,
timeoutMs: 4000,
validators: zodClient(spSchema),
onError({ result }) {
// TODO:
// message.set(result.error.message)
Expand All @@ -48,6 +52,7 @@ const {
delayed,
timeout,
constraints,
formId,
enhance,
capture,
restore,
Expand All @@ -67,6 +72,40 @@ function handlePasted(event: CustomEvent) {
$formData.country = content.country;
}
const schema = {
title: 'provider specialization',
type: 'object',
properties: {
specializations: {
type: 'array',
items: {
type: 'string',
},
},
},
};
const schema2 = {
$id: 'https://example.com/person.schema.json',
$schema: 'https://json-schema.org/draft/2020-12/schema',
title: 'Person',
type: 'object',
properties: {
firstName: {
type: 'string',
description: "The person's first name.",
},
lastName: {
type: 'string',
description: "The person's last name.",
},
age: {
description: 'Age in years which must be equal to or greater than zero.',
type: 'integer',
minimum: 0,
},
},
};
// Functions
const handlePaste = async (event: ClipboardEvent) => {
event.preventDefault();
Expand All @@ -80,15 +119,25 @@ const handlePaste = async (event: ClipboardEvent) => {
$: loadingState.setFormLoading($delayed);
</script>

<svelte:window on:paste={handlePaste} />
<!-- <svelte:window on:paste={handlePaste} /> -->

<div class="page-container">
<div class="page-section">
<header class="flex justify-between">
<h1 class="h1">Smart Paste Demo</h1>
</header>

<SmartSupport />

<!-- Form -->
<form method="POST" use:enhance class="card shadow-lg">
<form
method="POST"
class="card shadow-lg"
use:enhance
use:smartPast={{api: '/api/smartpast', schema}}
on:smartPast={(e) => console.log(e)}
on:error={(e) => console.log(e)}
>
<header class="card-header">
<!-- Form Level Errors / Messages -->
<Alerts errors={$errors._errors} message={$message} />
Expand Down Expand Up @@ -275,7 +324,8 @@ $: loadingState.setFormLoading($delayed);
isTainted: isTainted,
submitting: $submitting,
delayed: $delayed,
timeout: $timeout
timeout: $timeout,
formId: $formId,
}}
/>
<br />
Expand All @@ -286,6 +336,7 @@ $: loadingState.setFormLoading($delayed);
<SuperDebug label="Errors" status={false} data={$errors} />
<br />
<SuperDebug label="Constraints" status={false} data={$constraints} />
<br />
</DebugShell>
</div>
</div>
13 changes: 13 additions & 0 deletions apps/console/src/routes/(app)/ai/pastesmart/schema.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { z } from 'zod';

export const spSchema = z.object({
firstName: z.string().nullish(),
lastName: z.string().nullish(),
phoneNumber: z.string().nullish(),
line1: z.string().nullish(),
line2: z.string().nullish(),
city: z.string().nullish(),
state: z.string().nullish(),
zip: z.string().nullish(),
country: z.string().nullish(),
});
4 changes: 3 additions & 1 deletion apps/console/src/routes/(app)/ai/smart/+page.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ const {
tainted,
submitting,
formId,
isTainted,
capture,
restore,
} = form;
Expand All @@ -72,7 +73,7 @@ $: loadingState.setFormLoading($delayed);
<Smart.Support />

<!-- Form -->
<form method="POST" use:enhance class="card shadow-lg">
<form method="POST" class="card shadow-lg" use:enhance>
<header class="card-header">
<!-- Form Level Errors / Messages -->
<Alerts errors={$errors._errors} message={$message} />
Expand Down Expand Up @@ -215,6 +216,7 @@ $: loadingState.setFormLoading($delayed);
status={false}
data={{
message: $message,
isTainted: isTainted,
submitting: $submitting,
delayed: $delayed,
timeout: $timeout,
Expand Down
117 changes: 117 additions & 0 deletions packages/skeleton-ui/src/actions/clipboard.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
import { getElement, getTextContent, setTextContent } from '#utils';

import type { ActionReturn } from 'svelte/action';
import type { ElementOrSelector } from '#utils';

interface CopyAttributes {
'on:!copy'?: (event: CustomEvent<string>) => void;
}

interface CutAttributes {
'on:!cut'?: (event: CustomEvent<string>) => void;
}

interface PasteAttributes {
'on:!paste'?: (event: CustomEvent<string>) => void;
}

/**
* Copies the text content of `target` to the clipboard when `node` is clicked.
* If `target` is not provided, `node`'s textContent is copied. When `target` is a string, it is used as a query selector.
* Also dispatches a `!copy` event on 'node' with the copied text as `detail`.
* When `target` is an input or textarea, its value is copied. Otherwise, textContent is copied.
*
* Example:
* ```svelte
* <input id="input" value="Some value" bind:this={targetElement} />
* <button use:copy={"#input"} on:!copy={handler} />
* <button use:copy={targetElement} />
* <p use:copy>Some text</p>
*```
*/
export function copy(node: HTMLElement, target?: ElementOrSelector): ActionReturn<ElementOrSelector, CopyAttributes> {
let targetNode = getElement(target, node);

async function handleClick() {
const text = getTextContent(targetNode);
await navigator.clipboard.writeText(text);
node.dispatchEvent(new CustomEvent('!copy', { detail: { text } }));
}

node.addEventListener('click', handleClick);

return {
update: (newTarget: ElementOrSelector) => {
targetNode = getElement(newTarget, node);
},
destroy: () => node.removeEventListener('click', handleClick),
};
}

/**
* Cuts the text content of `target` to the clipboard when `node` is clicked.
* If `target` is not provided, `node`'s textContent is cut. When `target` is a string, it is used as a query selector.
* Also dispatches a `!cut` event on 'node' with the cut text as `detail`.
* When `target` is an input or textarea, its value is cut. Otherwise, textContent is cut.
* The original value or textArea is replaced with an empty string.
*
* Example:
* ```svelte
* <input id="input" value="Some value" bind:this={targetElement} />
* <button use:cut={"#input"} on:!cut={handler} />
* <button use:cut={targetElement} />
* <p use:cut>Some text</p>
*```
*/
export function cut(node: HTMLElement, target?: ElementOrSelector): ActionReturn<ElementOrSelector, CutAttributes> {
let targetNode = getElement(target, node);

async function handleClick() {
const text = getTextContent(targetNode);
await navigator.clipboard.writeText(text);
node.dispatchEvent(new CustomEvent('!cut', { detail: text }));
setTextContent(targetNode, '');
}

node.addEventListener('click', handleClick);

return {
update: (newTarget: ElementOrSelector) => {
targetNode = getElement(newTarget, node);
},
destroy: () => node.removeEventListener('click', handleClick),
};
}

/**
* Pastes the text content of the clipboard to `target` when `node` is clicked.
* If `target` is not provided, the clipboard contents are pasted to `node`'s textContent. When `target` is a string, it is used as a query selector.
* Also dispatches a `!paste` event on 'node' with the pasted text as `detail`.
* When `target` is an input or textarea, its value is pasted. Otherwise, textContent is pasted.
*
* Example:
* ```svelte
* <input id="input" bind:this={targetElement} />
* <button use:paste={"#input"} on:!paste={handler} />
* <button use:paste={targetElement} />
* <p use:paste />
*```
*/
export function paste(node: HTMLElement, target?: ElementOrSelector): ActionReturn<ElementOrSelector, PasteAttributes> {
let targetNode = getElement(target, node);

async function handleClick() {
const text = await navigator.clipboard.readText();
node.dispatchEvent(new CustomEvent('!paste', { detail: text }));
setTextContent(targetNode, text);
}

node.addEventListener('click', handleClick);

return {
update: (newTarget: ElementOrSelector) => {
targetNode = getElement(newTarget, node);
},
destroy: () => node.removeEventListener('click', handleClick),
};
}
59 changes: 59 additions & 0 deletions packages/skeleton-ui/src/utils/element.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { browser } from './env.js';

export type ElementOrSelector = HTMLElement | string | undefined;

/**
* Returns the target node from the provided target or the fallback node.
*/
export function getElement(target: ElementOrSelector, fallback: HTMLElement): HTMLElement;
export function getElement(target: ElementOrSelector): HTMLElement | undefined;
export function getElement(target: ElementOrSelector, fallback?: HTMLElement) {
return (typeof target === 'string' ? document.querySelector(target) : target) || fallback;
}

/**
* Returns the text content of the target node. If the target is an input or textarea, its value is returned. Otherwise, textContent is returned.
*/
export function getTextContent(target: Element) {
return target instanceof HTMLInputElement || target instanceof HTMLTextAreaElement
? target.value
: target.textContent || '';
}

/**
* Sets the text content of the target node. If the target is an input or textarea, its value is set. Otherwise, textContent is set.
*/
export function setTextContent(target: Element, text: string) {
if (target instanceof HTMLInputElement || target instanceof HTMLTextAreaElement) {
target.value = text;
} else {
target.textContent = text;
}
}

// This list originates from: https://stackoverflow.com/a/30753870
const focusableElements = `
a[href]:not([tabindex='-1']),
area[href]:not([tabindex='-1']),
input:not([disabled]):not([tabindex='-1']),
select:not([disabled]):not([tabindex='-1']),
textarea:not([disabled]):not([tabindex='-1']),
button:not([disabled]):not([tabindex='-1']),
iframe:not([tabindex='-1']),
[tabindex]:not([tabindex='-1']),
[contentEditable=true]:not([tabindex='-1'])
`;

/**
* Returns true if the node is focusable.
*/
export function isFocusable(node: HTMLElement) {
return node && node.matches(focusableElements);
}

/**
* Returns an array with all focusable children of the node.
*/
export function getFocusableChildren(node: HTMLElement): HTMLElement[] {
return node ? Array.from(node.querySelectorAll(focusableElements)) : [];
}
Loading

0 comments on commit d56473f

Please sign in to comment.