coveo-search-ui
Version:
Coveo JavaScript Search Framework
314 lines (275 loc) • 11 kB
text/typescript
import 'styling/_FieldTable';
import { each, uniqueId } from 'underscore';
import { exportGlobally } from '../../GlobalExports';
import { IQueryResult } from '../../rest/QueryResult';
import { l } from '../../strings/Strings';
import { AccessibleButton } from '../../utils/AccessibleButton';
import { $$ } from '../../utils/Dom';
import { QueryUtils } from '../../utils/QueryUtils';
import { SVGDom } from '../../utils/SVGDom';
import { SVGIcons } from '../../utils/SVGIcons';
import { Component } from '../Base/Component';
import { IComponentBindings } from '../Base/ComponentBindings';
import { ComponentOptions } from '../Base/ComponentOptions';
import { Initialization } from '../Base/Initialization';
import { FieldValue, IFieldValueOptions } from '../FieldValue/FieldValue';
export interface IFieldTableOptions {
allowMinimization: boolean;
expandedTitle: string;
minimizedTitle: string;
minimizedByDefault: boolean;
}
/**
* The FieldTable component displays a set of {@link FieldValue} components in a table that can optionally be
* expandable and minimizable. This component automatically takes care of not displaying empty field values.
*
* This component is a result template component (see [Result Templates](https://docs.coveo.com/en/413/)).
*
* **Example:**
*
* ```
* // This is the FieldTable component itself, which holds a list of table rows.
* // Each row is a FieldValue component.
* <table class='CoveoFieldTable'>
* // Items
* <tr data-field='@sysdate' data-caption='Date' data-helper='dateTime' />
* <tr data-field='@sysauthor' data-caption='Author' />
* <tr data-field='@clickuri' data-html-value='true' data-caption='URL' data-helper='anchor' data-helper-options='{text: \"<%= raw.syssource %>\" , target:\"_blank\"}'>
* </table>
* ```
*/
export class FieldTable extends Component {
static ID = 'FieldTable';
static doExport = () => {
exportGlobally({
FieldTable: FieldTable,
FieldValue: FieldValue
});
};
/**
* The options for the component
* @componentOptions
*/
static options: IFieldTableOptions = {
/**
* Specifies whether to allow the minimization (collapsing) of the FieldTable.
*
* If you set this option to `false`, the component will not create the **Minimize** / **Expand** toggle links.
*
* See also {@link FieldTable.options.expandedTitle}, {@link FieldTable.options.minimizedTitle} and
* {@link FieldTable.options.minimizedByDefault}.
*
* Default value is `true`.
*/
allowMinimization: ComponentOptions.buildBooleanOption({ defaultValue: true }),
/**
* If {@link FieldTable.options.allowMinimization} is `true`, specifies the caption to show on the **Minimize** link
* (the link that appears when the FieldTable is expanded).
*
* Default value is `"Details"`.
*/
expandedTitle: ComponentOptions.buildLocalizedStringOption({
localizedString: () => l('Details'),
depend: 'allowMinimization'
}),
/**
* If {@link FieldTable.options.allowMinimization} is `true`, specifies the caption to show on the **Expand** link
* (the link that appears when the FieldTable is minimized).
*
* Default value is `"Details"`.
*/
minimizedTitle: ComponentOptions.buildLocalizedStringOption({
localizedString: () => l('Details'),
depend: 'allowMinimization'
}),
/**
* If {@link FieldTable.options.allowMinimization} is `true`, specifies whether to minimize the table by default.
*
* Default value is `undefined`, and the FieldTable will collapse by default if the result it is associated with has
* a non-empty excerpt.
*/
minimizedByDefault: ComponentOptions.buildBooleanOption({ depend: 'allowMinimization' })
};
private toggleButton: HTMLElement;
private toggleCaption: HTMLElement;
private toggleButtonSVGContainer: HTMLElement;
private toggleButtonInsideTable: HTMLElement;
private toggleContainer: HTMLElement;
private toggleContainerHeight: number;
/**
* Creates a new FieldTable.
* @param element The HTMLElement on which to instantiate the component.
* @param options The options for the FieldTable component.
* @param bindings The bindings that the component requires to function normally. If not set, these will be
* automatically resolved (with a slower execution time).
* @param result The result to associate the component with.
*/
constructor(
public element: HTMLElement,
public options?: IFieldTableOptions,
bindings?: IComponentBindings,
public result?: IQueryResult
) {
super(element, ValueRow.ID, bindings);
this.options = ComponentOptions.initComponentOptions(element, FieldTable, options);
var rows = $$(this.element).findAll('tr[data-field]');
each(rows, (e: HTMLElement) => {
new ValueRow(e, {}, bindings, result);
});
if ($$(this.element).find('tr') == null) {
$$(element).detach();
}
if (this.isTogglable()) {
const className = 'coveo-field-table-toggle-container';
this.toggleContainer = $$('div', { className, id: uniqueId(className) }).el;
this.buildToggle();
$$(this.toggleContainer).insertBefore(this.element);
this.toggleContainer.appendChild(this.element);
this.toggleContainer.appendChild(this.toggleButtonInsideTable);
}
}
public get isExpanded() {
return !this.toggleButton || this.toggleButton.getAttribute('aria-expanded') === 'true';
}
public set isExpanded(expanded: boolean) {
if (this.toggleButton) {
this.toggleButton.setAttribute('aria-expanded', expanded.toString());
}
}
/**
* Toggles between expanding (showing) and minimizing (collapsing) the FieldTable.
*
* @param anim Specifies whether to show a sliding animation when toggling the display of the FieldTable.
*/
public toggle(anim = false) {
this.isExpanded ? this.minimize(anim) : this.expand(anim);
}
/**
* Expands (shows) the FieldTable,
* @param anim Specifies whether to show a sliding animation when expanding the FieldTable.
*/
public expand(anim = false) {
if (this.isTogglable()) {
this.isExpanded = true;
this.toggleCaption.textContent = this.options.expandedTitle;
SVGDom.addClassToSVGInContainer(this.toggleButtonSVGContainer, 'coveo-opened');
SVGDom.addClassToSVGInContainer(this.toggleButtonInsideTable, 'coveo-opened');
anim ? this.slideToggle(true) : this.slideToggle(true, false);
}
}
/**
* Minimizes (collapses) the FieldTable.
* @param anim Specifies whether to show a sliding animation when minimizing the FieldTable.
*/
public minimize(anim = false) {
if (this.isTogglable()) {
this.isExpanded = false;
this.toggleCaption.textContent = this.options.minimizedTitle;
SVGDom.removeClassFromSVGInContainer(this.toggleButtonSVGContainer, 'coveo-opened');
SVGDom.removeClassFromSVGInContainer(this.toggleButtonInsideTable, 'coveo-opened');
anim ? this.slideToggle(false) : this.slideToggle(false, false);
}
}
/**
* Updates the toggle height if the content was dynamically resized, so that the expanding and minimizing animation
* can match the new content size.
*/
public updateToggleHeight() {
this.updateToggleContainerHeight();
this.isExpanded ? this.expand() : this.minimize();
}
protected isTogglable() {
if (this.options.allowMinimization) {
return true;
}
return false;
}
private buildToggle() {
this.toggleCaption = $$('span', {
className: 'coveo-field-table-toggle-caption'
}).el;
this.toggleButton = $$('div', {
className: 'coveo-field-table-toggle coveo-field-table-toggle-down',
ariaControls: this.toggleContainer.id
}).el;
this.toggleButtonSVGContainer = $$('span', null, SVGIcons.icons.arrowDown).el;
SVGDom.addClassToSVGInContainer(this.toggleButtonSVGContainer, 'coveo-field-table-toggle-down-svg');
this.toggleButton.appendChild(this.toggleCaption);
this.toggleButton.appendChild(this.toggleButtonSVGContainer);
$$(this.toggleButton).insertBefore(this.element);
this.toggleButtonInsideTable = $$(
'span',
{ className: 'coveo-field-table-toggle coveo-field-table-toggle-up' },
SVGIcons.icons.arrowUp
).el;
SVGDom.addClassToSVGInContainer(this.toggleButtonInsideTable, 'coveo-field-table-toggle-up-svg');
if (this.options.minimizedByDefault === true) {
this.isExpanded = false;
} else if (this.options.minimizedByDefault === false) {
this.isExpanded = true;
} else {
this.isExpanded = !QueryUtils.hasExcerpt(this.result);
}
requestAnimationFrame(() => this.updateToggleHeight());
const toggleAction = () => this.toggle(true);
new AccessibleButton()
.withElement(this.toggleButton)
.withSelectAction(() => toggleAction())
.withOwner(this.bind)
.withLabel(l('Details'))
.build();
$$(this.toggleButtonInsideTable).on('click', toggleAction);
}
private slideToggle(visible: boolean = true, anim: boolean = true) {
if (!anim) {
$$(this.toggleContainer).addClass('coveo-no-transition');
}
if (visible) {
this.toggleContainer.style.display = 'block';
this.toggleContainer.style.height = this.containerHeight;
} else {
this.toggleContainer.style.height = '0';
}
if (!anim) {
this.toggleContainer.offsetHeight; // Force reflow
$$(this.toggleContainer).removeClass('coveo-no-transition');
}
}
private get containerHeight() {
if (!this.toggleContainerHeight) {
this.updateToggleContainerHeight();
}
return this.toggleContainerHeight + 'px';
}
private updateToggleContainerHeight() {
this.toggleContainerHeight = this.toggleContainer.scrollHeight;
}
}
Initialization.registerAutoCreateComponent(FieldTable);
export interface IValueRowOptions extends IFieldValueOptions {
caption?: string;
}
class ValueRow extends FieldValue {
static ID = 'ValueRow';
static options: IValueRowOptions = {
caption: ComponentOptions.buildStringOption({
postProcessing: (value, options) => value || options.field.substr(1)
})
};
static parent = FieldValue;
private valueContainer: HTMLElement;
constructor(public element: HTMLElement, public options: IValueRowOptions, bindings?: IComponentBindings, public result?: IQueryResult) {
super(element, options, bindings, result, ValueRow.ID);
this.options = ComponentOptions.initComponentOptions(element, ValueRow, options);
var caption = $$('th').el;
caption.appendChild(document.createTextNode(this.options.caption.toLocaleString()));
this.element.insertBefore(caption, this.getValueContainer());
}
protected getValueContainer() {
if (this.valueContainer == null) {
this.valueContainer = document.createElement('td');
this.element.appendChild(this.valueContainer);
}
return this.valueContainer;
}
}