@syncfusion/ej2-buttons
Version:
A package of feature-rich Essential JS 2 components such as Button, CheckBox, RadioButton and Switch.
362 lines (338 loc) • 15.6 kB
text/typescript
// eslint-disable-next-line @typescript-eslint/triple-slash-reference
///<reference path='../button/button-model.d.ts'/>
import { Button } from '../button/button';
import { EventHandler, Property } from '@syncfusion/ej2-base';
import { SmartPasteButtonModel } from './smart-paste-button-model';
/*
* Smart Paste Button
*/
interface FieldInfo {
fieldName?: string
description?: string
element?: HTMLElement
allowedValues?: string[]
type?: string
}
interface NavigatorClipboard {
clipboard: {
readText(): Promise<string>;
};
}
export interface ChatOptions {
messages: {
role: string;
content: string;
}[];
temperature?: number;
topP?: number;
maxTokens?: number;
frequencyPenalty?: number;
presencePenalty?: number;
stop?: string[];
}
export class SmartPasteButton extends Button {
/**
* Callback function to get the form response data from the server to process the smart paste.
*
* @param {ChatOptions} settings - Specifies the chat options
* @returns {string} - Returns the string
*/
@Property()
public aiAssistHandler: Function;
/**
* Constructor for creating the widget
*
* @private
* @param {SmartPasteButtonModel} options - Specifies Smart paste button model
* @param {string | HTMLButtonElement} element - Specifies target element
*/
constructor(options?: SmartPasteButtonModel, element?: string | HTMLButtonElement) {
super(options, <string | HTMLButtonElement>element);
}
protected wireEvents(): void {
EventHandler.add(this.element, 'click', this.smartPasteBtnClickHandler, this);
}
protected unWireEvents(): void {
EventHandler.remove(this.element, 'click', this.smartPasteBtnClickHandler);
}
private async smartPasteBtnClickHandler(args: MouseEvent): Promise<void> {
const target: HTMLButtonElement = args.target as HTMLButtonElement;
const formElement: HTMLFormElement = target.closest('form') as HTMLFormElement;
if (!formElement) {
return;
}
const formFields: FieldInfo[] = this.getFormFields(formElement);
if (formFields.length === 0) {
return;
}
const clipboardContent: string = await this.getClipboardContent().then((text: string) => {
return text;
});
if (clipboardContent !== 'Clipboard API not supported' && clipboardContent !== 'Clipboard access failed') {
try {
this.disabled = true;
const fieldsData: FieldInfo[] = formFields.map((field: FieldInfo) => {
return {
fieldName: field.fieldName,
description: field.description,
allowedValues: field.allowedValues,
type: field.type
};
});
const systemRole: string = `
Current date: ${new Date().toLocaleDateString('en-US', { year: 'numeric', month: 'long', day: 'numeric' })}
Each response line matches the following format:
FIELD identifier^^^value
Give a response with the following lines only, with values inferred from USER_DATA:
${this.formatFields(fieldsData)}
END_RESPONSE
Do not explain how the values were determined.
For fields without any corresponding information in USER_DATA, use value NO_DATA.`;
const userRole: string = `
USER_DATA: ${clipboardContent}
`;
const settings: ChatOptions = {
messages: [
{ role: 'system', content: systemRole },
{ role: 'user', content: userRole }
],
temperature: 0.0,
topP: 1.0,
maxTokens: 2000,
frequencyPenalty: 0.1,
presencePenalty: 0.0,
stop: ['END_RESPONSE']
};
if (typeof this.aiAssistHandler === 'function') {
const response: string = await this.aiAssistHandler(settings);
if (typeof response === 'string' && response !== '') {
this.setFormFields(formElement, formFields, response);
}
}
} finally {
this.disabled = false;
}
}
}
private formatFields(fields: FieldInfo[]): string {
const result: string[] = [];
fields.forEach((field: FieldInfo) => {
const fieldOutput: string[] = [];
fieldOutput.push(`\nFIELD ${field.fieldName}^^^`);
if (field.description) {
fieldOutput.push(`The ${field.description}`);
}
if (field.allowedValues && field.allowedValues.length > 0) {
fieldOutput.push(' (multiple choice, with allowed values: ');
fieldOutput.push(field.allowedValues.map((val: string) => `${val}`).join(','));
fieldOutput.push(')');
} else {
fieldOutput.push(` of type ${field.type}`);
}
result.push(fieldOutput.join(''));
});
return result.join('');
}
private setFormFields(form: HTMLFormElement, formFields: FieldInfo[], response: string): void {
const responseData: { [key: string]: string } = {};
const fieldPrefix: string = 'FIELD ';
let currentField: string | null = null;
response.split('\n').forEach((line: string) => {
if (line.startsWith(fieldPrefix)) {
const parts: string[] = line.substring(fieldPrefix.length).split('^^^');
if (parts.length === 2) {
responseData[`${parts[0]}`] = parts[1];
currentField = parts[0];
}
} else if (currentField) {
responseData[`${currentField}`] += '\n' + line;
}
});
formFields.forEach((field: FieldInfo) => {
let value: string = responseData[field.fieldName];
if (value !== undefined) {
value = value.trim();
if (value === 'NO_DATA') {
return;
}
if (field.element instanceof HTMLInputElement && field.element.type === 'radio') {
const radioButton: HTMLInputElement = this.findRadioButton(form, field.element.name, value);
if (radioButton) {
this.updateElementValue(radioButton, 'true');
}
} else {
this.updateElementValue(field.element, value);
}
}
});
}
private findRadioButton(form: HTMLFormElement, name: string, value: string): HTMLInputElement | null {
const radioButtons: { elem: HTMLInputElement; text: string }[] = Array.from(form.querySelectorAll('input[type=radio]'))
.filter((radio: HTMLElement) => radio instanceof HTMLInputElement && radio.name === name)
.map((radio: HTMLInputElement) => ({ elem: radio, text: this.getElementDescription(form, radio) }));
const exactMatch: { elem: HTMLInputElement } = radioButtons.find((radio: { text: string }) => radio.text === value);
if (exactMatch) {
return exactMatch.elem;
}
const partialMatch: { elem: HTMLInputElement }[] = radioButtons.filter((radio: { text: string }) => radio.text &&
radio.text.includes(value));
if (partialMatch.length === 1) {
return partialMatch[0].elem;
}
return null;
}
private triggerBeforeChange(element: Element): void {
element.dispatchEvent(new CustomEvent('beforeinput', {
bubbles: true,
detail: {
fromSmartComponents: true
}
}));
}
private triggerAfterChange(element: Element): void {
element.dispatchEvent(new CustomEvent('input', {
bubbles: true,
detail: {
fromSmartComponents: true
}
}));
element.dispatchEvent(new CustomEvent('change', {
bubbles: true,
detail: {
fromSmartComponents: true
}
}));
}
private updateElementValue(element: HTMLElement, value: string): void {
const isEjsControl: boolean = element.classList.contains('e-control');
if (element instanceof HTMLInputElement && (element.type === 'radio' || element.type === 'checkbox')) {
const responseValue: string | undefined = value == null ? undefined : value.toString().toLowerCase();
const isResponseValue: boolean = responseValue === 'true' || responseValue === 'yes' || responseValue === 'on';
if (element.checked !== isResponseValue) {
this.triggerBeforeChange(element);
if (isEjsControl) {
(((element as unknown) as { [key: string]: object[] })['ej2_instances'] as HTMLInputElement[])[0].checked = isResponseValue;
} else {
element.checked = isResponseValue;
}
this.triggerAfterChange(element);
}
} else if (element instanceof HTMLSelectElement) {
const optionText: string = value.toString();
let index: number | null = null;
const options: HTMLOptionElement[] = Array.from(element.querySelectorAll('option'));
const exactMatch: HTMLOptionElement[] = options.filter((option: HTMLOptionElement) => option.textContent === optionText);
if (exactMatch.length > 0) {
index = options.indexOf(exactMatch[0]);
} else {
const partialMatch: HTMLOptionElement[] = options.filter((option: HTMLOptionElement) => option.textContent &&
option.textContent.indexOf(optionText) >= 0);
if (partialMatch.length === 1) {
index = options.indexOf(partialMatch[0]);
}
}
if (index !== null && element.selectedIndex !== index) {
this.triggerBeforeChange(element);
if (isEjsControl) {
(((element as unknown) as { [key: string]: object[] })['ej2_instances'] as any[])[0].index = index;
}
else {
element.selectedIndex = index;
}
this.triggerAfterChange(element);
}
} else {
this.triggerBeforeChange(element);
if (element.classList.contains('e-rating') || element.classList.contains('e-colorpicker')) {
(((element as unknown) as { [key: string]: object[] })['ej2_instances'] as HTMLInputElement[])[0].value = value;
} else {
(element as HTMLInputElement).value = value;
}
this.triggerAfterChange(element);
}
element.focus();
}
private getFormFields(form: HTMLFormElement): FieldInfo[] {
const fields: FieldInfo[] = [];
let uniqueCount: number = 0;
form.querySelectorAll('input, select, textarea').forEach((element: Element) => {
if (!(element instanceof HTMLInputElement || element instanceof HTMLSelectElement || element instanceof HTMLTextAreaElement)) {
return;
}
if (element.type === 'hidden' || this.isFieldIgnore(element)) {
return;
}
const isRadioButton: boolean = element.type === 'radio';
const identifier: string = isRadioButton ? element.name : element.id || element.name || `unidentified_${++uniqueCount}`;
if (isRadioButton && fields.find((field: FieldInfo) => field.fieldName === identifier)) {
return;
}
let FieldDescription: string = null;
if (!isRadioButton) {
FieldDescription = this.getElementDescription(form, element);
if (!FieldDescription) {
return;
}
}
const fieldInfo: FieldInfo = {
fieldName: element.name,
description: FieldDescription,
element: element,
type: element.type === 'checkbox' ? 'boolean' : element.type === 'number' ? 'number' : 'string'
};
if (element instanceof HTMLSelectElement) {
const options: HTMLOptionElement[] = Array.from(element.querySelectorAll('option')).filter((option: HTMLOptionElement) => option.value);
fieldInfo.allowedValues = options.map((option: HTMLOptionElement) => option.textContent);
fieldInfo.type = 'fixed-choices';
} else if (isRadioButton) {
fieldInfo.allowedValues = [];
fieldInfo.type = 'fixed-choices';
Array.from(form.querySelectorAll('input[type=radio]')).forEach((radio: HTMLInputElement) => {
if (radio.name === identifier) {
const radioDescription: string = this.getElementDescription(form, radio);
if (radioDescription) {
fieldInfo.allowedValues.push(radioDescription);
}
}
});
}
fields.push(fieldInfo);
});
return fields;
}
private isFieldIgnore(element: Element): boolean {
return element.hasAttribute('data-smartpaste-ignore') ||
(element.hasAttribute('aria-disabled') && element.getAttribute('aria-disabled') === 'true') ||
(element.hasAttribute('disabled')) || (element.hasAttribute('readonly')) ||
(element.hasAttribute('aria-readonly') && element.getAttribute('aria-readonly') === 'true') ||
(element.hasAttribute('aria-hidden') && element.getAttribute('aria-hidden') === 'true');
}
private getElementDescription(form: HTMLFormElement, element: HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement): string {
if (element.hasAttribute('data-smartpaste-description')) {
return element.getAttribute('data-smartpaste-description') as string;
}
if ((element instanceof HTMLInputElement || element instanceof HTMLTextAreaElement) && element.placeholder) {
return element.placeholder;
}
const label: Element = form.querySelector(`label[for="${element.id}"]`);
if (label) {
return (label.textContent as string).trim();
}
return element.name || element.id;
}
private async getClipboardContent(): Promise<string> {
const navigatorObj: NavigatorClipboard = navigator as Navigator & NavigatorClipboard;
const customClipboard: HTMLInputElement | HTMLTextAreaElement = document.getElementById('custom-clipboard') as HTMLInputElement | HTMLTextAreaElement;
if (customClipboard && customClipboard.value) {
return customClipboard.value;
} else if (typeof window !== 'undefined' && navigatorObj.clipboard && navigatorObj.clipboard.readText) {
try {
return await navigatorObj.clipboard.readText();
} catch (error) {
return 'Clipboard access failed';
}
} else {
return 'Clipboard API not supported';
}
}
}