feat(popover-edit): allow tabbing from popup to next/previous cell (#15793)

This commit is contained in:
Karl Seamon
2019-04-16 00:47:24 -04:00
committed by Jeremy Elbourn
parent 82b56793f8
commit d3d026b42d
10 changed files with 445 additions and 61 deletions
@@ -12,6 +12,7 @@ import {audit, distinctUntilChanged, filter, map, share} from 'rxjs/operators';
import {CELL_SELECTOR, ROW_SELECTOR} from './constants';
import {closest} from './polyfill';
import {EditRef} from './edit-ref';
/** The delay between mouse out events and hiding hover content. */
const DEFAULT_MOUSE_OUT_DELAY_MS = 30;
@@ -30,6 +31,12 @@ export class EditEventDispatcher {
/** A subject that emits mouse move events for table rows. */
readonly mouseMove = new Subject<Element|null>();
/** The EditRef for the currently active edit lens (if any). */
get editRef(): EditRef<any>|null {
return this._editRef;
}
private _editRef: EditRef<any>|null = null;
/** The table cell that has an active edit lens (or null). */
private _currentlyEditing: Element|null = null;
@@ -67,6 +74,20 @@ export class EditEventDispatcher {
}
}
/** Sets the currently active EditRef. */
setActiveEditRef(ref: EditRef<any>) {
this._editRef = ref;
}
/** Unsets the currently active EditRef, if the specified editRef is active. */
unsetActiveEditRef(ref: EditRef<any>) {
if (this._editRef !== ref) {
return;
}
this._editRef = null;
}
/**
* Gets an Observable that emits true when the specified element's row
* is being hovered over and false when not. Hovering is defined as when
+13 -1
View File
@@ -23,6 +23,10 @@ export class EditRef<FormValue> implements OnDestroy {
private readonly _finalValueSubject = new Subject<FormValue>();
readonly finalValue: Observable<FormValue> = this._finalValueSubject.asObservable();
/** Emits when the user tabs out of this edit lens before closing. */
private readonly _blurredSubject = new Subject<void>();
readonly blurred: Observable<void> = this._blurredSubject.asObservable();
/** The value to set the form back to on revert. */
private _revertFormValue: FormValue;
@@ -37,7 +41,9 @@ export class EditRef<FormValue> implements OnDestroy {
constructor(
@Self() private readonly _form: ControlContainer,
private readonly _editEventDispatcher: EditEventDispatcher) {}
private readonly _editEventDispatcher: EditEventDispatcher) {
this._editEventDispatcher.setActiveEditRef(this);
}
/**
* Called by the host directive's OnInit hook. Reads the initial state of the
@@ -57,6 +63,7 @@ export class EditRef<FormValue> implements OnDestroy {
}
ngOnDestroy(): void {
this._editEventDispatcher.unsetActiveEditRef(this);
this._finalValueSubject.next(this._form.value);
this._finalValueSubject.complete();
}
@@ -76,6 +83,11 @@ export class EditRef<FormValue> implements OnDestroy {
this._editEventDispatcher.editing.next(null);
}
/** Notifies the active edit that the user has moved focus out of the lens. */
blur(): void {
this._blurredSubject.next();
}
/**
* Closes the edit if the enter key is not down.
* Otherwise, sets _closePending to true so that the edit will close on the
@@ -0,0 +1,74 @@
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {Inject, Injectable, NgZone} from '@angular/core';
import {DOCUMENT} from '@angular/common';
import {FocusTrap, InteractivityChecker} from '@angular/cdk/a11y';
import {Observable, Subject} from 'rxjs';
/** Value indicating whether focus left the target area before or after the enclosed elements. */
export const enum FocusEscapeNotifierDirection {
START,
END,
}
/**
* Like FocusTrap, but rather than trapping focus within a dom region, notifies subscribers when
* focus leaves the region.
*/
export class FocusEscapeNotifier extends FocusTrap {
private _escapeSubject = new Subject<FocusEscapeNotifierDirection>();
constructor(
element: HTMLElement,
checker: InteractivityChecker,
ngZone: NgZone,
document: Document) {
super(element, checker, ngZone, document, true /* deferAnchors */);
// The focus trap adds "anchors" at the beginning and end of a trapped region that redirect
// focus. We override that redirect behavior here with simply emitting on a stream.
this.startAnchorListener = () => {
this._escapeSubject.next(FocusEscapeNotifierDirection.START);
return true;
};
this.endAnchorListener = () => {
this._escapeSubject.next(FocusEscapeNotifierDirection.END);
return true;
};
this.attachAnchors();
}
escapes(): Observable<FocusEscapeNotifierDirection> {
return this._escapeSubject.asObservable();
}
}
/** Factory that allows easy instantiation of focus escape notifiers. */
@Injectable({providedIn: 'root'})
export class FocusEscapeNotifierFactory {
private _document: Document;
constructor(
private _checker: InteractivityChecker,
private _ngZone: NgZone,
@Inject(DOCUMENT) _document: any) {
this._document = _document;
}
/**
* Creates a focus escape notifier region around the given element.
* @param element The element around which focus will be monitored.
* @returns The created focus escape notifier instance.
*/
create(element: HTMLElement): FocusEscapeNotifier {
return new FocusEscapeNotifier(element, this._checker, this._ngZone, this._document);
}
}
@@ -61,6 +61,7 @@ export class CdkEditControl<FormValue> implements OnDestroy, OnInit {
ngOnInit(): void {
this.editRef.init(this.preservedFormValue);
this.editRef.finalValue.subscribe(this.preservedFormValueChange);
this.editRef.blurred.subscribe(() => this._handleBlur());
}
ngOnDestroy(): void {
@@ -97,7 +98,7 @@ export class CdkEditControl<FormValue> implements OnDestroy, OnInit {
switch (this.clickOutBehavior) {
case 'submit':
// Manually cause the form to submit before closing.
this.elementRef.nativeElement!.dispatchEvent(new Event('submit'));
this._triggerFormSubmit();
// Fall through
case 'close':
this.editRef.close();
@@ -106,6 +107,18 @@ export class CdkEditControl<FormValue> implements OnDestroy, OnInit {
break;
}
}
/** Triggers submit on tab out if clickOutBehavior is 'submit'. */
private _handleBlur(): void {
if (this.clickOutBehavior === 'submit') {
// Manually cause the form to submit before closing.
this._triggerFormSubmit();
}
}
private _triggerFormSubmit() {
this.elementRef.nativeElement!.dispatchEvent(new Event('submit'));
}
}
/** Reverts the form to its initial or previously submitted state on click. */
@@ -10,6 +10,7 @@ import {NgModule} from '@angular/core';
import {OverlayModule} from '@angular/cdk/overlay';
import {
CdkPopoverEdit,
CdkPopoverEditTabOut,
CdkRowHoverContent,
CdkEditable,
CdkEditOpen,
@@ -26,6 +27,7 @@ import {
const EXPORTED_DECLARATIONS = [
CdkPopoverEdit,
CdkPopoverEditTabOut,
CdkRowHoverContent,
CdkEditControl,
CdkEditRevert,
@@ -28,7 +28,13 @@ const NAME_EDIT_TEMPLATE = `
</div>
`;
const WEIGHT_EDIT_TEMPLATE = `<div> Just a placeholder </div>`;
const WEIGHT_EDIT_TEMPLATE = `
<div>
<form #f="ngForm" cdkEditControl>
<input>
</form>
</div>
`;
const CELL_TEMPLATE = `
{{element.name}}
@@ -40,7 +46,7 @@ const CELL_TEMPLATE = `
const POPOVER_EDIT_DIRECTIVE_NAME = `[cdkPopoverEdit]="nameEdit" [cdkPopoverEditColspan]="colspan"`;
const POPOVER_EDIT_DIRECTIVE_WEIGHT = `[cdkPopoverEdit]="weightEdit"`;
const POPOVER_EDIT_DIRECTIVE_WEIGHT = `[cdkPopoverEdit]="weightEdit" cdkPopoverEditTabOut`;
interface PeriodicElement {
name: string;
@@ -74,13 +80,13 @@ abstract class BaseTestComponent {
return getRows(this.table.nativeElement);
}
getEditCell(rowIndex = 0) {
getEditCell(rowIndex = 0, cellIndex = 1) {
const row = this.getRows()[rowIndex];
return getCells(row)[1];
return getCells(row)[cellIndex];
}
focusEditCell(rowIndex = 0) {
this.getEditCell(rowIndex).focus();
focusEditCell(rowIndex = 0, cellIndex = 1) {
this.getEditCell(rowIndex, cellIndex).focus();
}
getOpenButton(rowIndex = 0) {
@@ -91,10 +97,10 @@ abstract class BaseTestComponent {
this.getOpenButton(rowIndex)!.click();
}
openLens(rowIndex = 0) {
this.focusEditCell(rowIndex);
this.getEditCell(rowIndex).dispatchEvent(
new KeyboardEvent('keyup', {bubbles: true, key: 'Enter'}));
openLens(rowIndex = 0, cellIndex = 1) {
this.focusEditCell(rowIndex, cellIndex);
this.getEditCell(rowIndex, cellIndex)
.dispatchEvent(new KeyboardEvent('keyup', {bubbles: true, key: 'Enter'}));
flush();
}
@@ -102,6 +108,10 @@ abstract class BaseTestComponent {
return document.querySelector('.cdk-edit-pane');
}
getEditBoundingBox() {
return document.querySelector('.cdk-overlay-connected-position-bounding-box');
}
getInput() {
return document.querySelector('input') as HTMLInputElement|null;
}
@@ -366,68 +376,124 @@ describe('CDK Popover Edit', () => {
}));
});
describe('arrow key focus manipulation', () => {
const dispatchKey = (cell: HTMLElement, keyCode: number) =>
dispatchKeyboardEvent(cell, 'keydown', keyCode, cell);
describe('focus manipulation', () => {
const getRowCells = () => component.getRows().map(getCells);
it('moves focus up/down/left/right and prevents default', () => {
const rowCells = getRowCells();
describe('arrow keys', () => {
const dispatchKey = (cell: HTMLElement, keyCode: number) =>
dispatchKeyboardEvent(cell, 'keydown', keyCode, cell);
// Focus the upper-left editable cell.
rowCells[0][1].focus();
it('moves focus up/down/left/right and prevents default', () => {
const rowCells = getRowCells();
const downEvent = dispatchKey(rowCells[0][1], DOWN_ARROW);
expect(document.activeElement).toBe(rowCells[1][1]);
expect(downEvent.defaultPrevented).toBe(true);
// Focus the upper-left editable cell.
rowCells[0][1].focus();
const rightEvent = dispatchKey(rowCells[1][1], RIGHT_ARROW);
expect(document.activeElement).toBe(rowCells[1][2]);
expect(rightEvent.defaultPrevented).toBe(true);
const downEvent = dispatchKey(rowCells[0][1], DOWN_ARROW);
expect(document.activeElement).toBe(rowCells[1][1]);
expect(downEvent.defaultPrevented).toBe(true);
const upEvent = dispatchKey(rowCells[1][2], UP_ARROW);
expect(document.activeElement).toBe(rowCells[0][2]);
expect(upEvent.defaultPrevented).toBe(true);
const rightEvent = dispatchKey(rowCells[1][1], RIGHT_ARROW);
expect(document.activeElement).toBe(rowCells[1][2]);
expect(rightEvent.defaultPrevented).toBe(true);
const leftEvent = dispatchKey(rowCells[0][2], LEFT_ARROW);
expect(document.activeElement).toBe(rowCells[0][1]);
expect(leftEvent.defaultPrevented).toBe(true);
const upEvent = dispatchKey(rowCells[1][2], UP_ARROW);
expect(document.activeElement).toBe(rowCells[0][2]);
expect(upEvent.defaultPrevented).toBe(true);
const leftEvent = dispatchKey(rowCells[0][2], LEFT_ARROW);
expect(document.activeElement).toBe(rowCells[0][1]);
expect(leftEvent.defaultPrevented).toBe(true);
});
it('wraps around when reaching start or end of a row, skipping non-editable cells',
() => {
const rowCells = getRowCells();
// Focus the upper-right editable cell.
rowCells[0][2].focus();
dispatchKey(rowCells[0][2], RIGHT_ARROW);
expect(document.activeElement).toBe(rowCells[1][1]);
dispatchKey(rowCells[1][1], LEFT_ARROW);
expect(document.activeElement).toBe(rowCells[0][2]);
});
it('does not fall off top or bottom of the table', () => {
const rowCells = getRowCells();
// Focus the upper-left editable cell.
rowCells[0][1].focus();
dispatchKey(rowCells[0][1], UP_ARROW);
expect(document.activeElement).toBe(rowCells[0][1]);
// Focus the bottom-left editable cell.
rowCells[4][1].focus();
dispatchKey(rowCells[4][1], DOWN_ARROW);
expect(document.activeElement).toBe(rowCells[4][1]);
});
it('ignores non arrow key events', () => {
component.focusEditCell();
const cell = component.getEditCell();
expect(dispatchKey(cell, TAB).defaultPrevented).toBe(false);
});
});
it('wraps around when reaching start or end of a row, skipping non-editable cells', () => {
const rowCells = getRowCells();
describe('lens focus trapping behavior', () => {
const getFocusablePaneElements = () =>
Array.from(component.getEditBoundingBox()!.querySelectorAll(
'input, button, .cdk-focus-trap-anchor')) as HTMLElement[];
// Focus the upper-right editable cell.
rowCells[0][2].focus();
it('keeps focus within the lens by default', fakeAsync(() => {
// Open the name lens which has the default behavior.
component.openLens();
dispatchKey(rowCells[0][2], RIGHT_ARROW);
expect(document.activeElement).toBe(rowCells[1][1]);
const focusableElements = getFocusablePaneElements();
dispatchKey(rowCells[1][1], LEFT_ARROW);
expect(document.activeElement).toBe(rowCells[0][2]);
});
// Focus the last element (end focus trap anchor).
focusableElements[focusableElements.length - 1].focus();
flush();
it('does not fall off top or bottom of the table', () => {
const rowCells = getRowCells();
// Focus should have moved to the top of the lens.
expect(document.activeElement).toBe(focusableElements[1]);
expect(component.lensIsOpen()).toBe(true);
}));
// Focus the upper-left editable cell.
rowCells[0][1].focus();
it('moves focus to the next cell when focus leaves end of lens with cdkPopoverEditTabOut',
fakeAsync(() => {
// Open the weight lens which has tab out behavior.
component.openLens(0, 2);
dispatchKey(rowCells[0][1], UP_ARROW);
expect(document.activeElement).toBe(rowCells[0][1]);
const focusableElements = getFocusablePaneElements();
// Focus the bottom-left editable cell.
rowCells[4][1].focus();
dispatchKey(rowCells[4][1], DOWN_ARROW);
expect(document.activeElement).toBe(rowCells[4][1]);
});
// Focus the last element (end focus trap anchor).
focusableElements[focusableElements.length - 1].focus();
flush();
it('ignores non arrow key events', () => {
component.focusEditCell();
const cell = component.getEditCell();
// Focus should have moved to the next editable cell.
expect(document.activeElement).toBe(component.getEditCell(1, 1));
expect(component.lensIsOpen()).toBe(false);
}));
expect(dispatchKey(cell, TAB).defaultPrevented).toBe(false);
it(`moves focus to the previous cell when focus leaves end of lens with
cdkPopoverEditTabOut`, fakeAsync(() => {
// Open the weight lens which has tab out behavior.
component.openLens(0, 2);
const focusableElements = getFocusablePaneElements();
// Focus the first (start focus trap anchor).
focusableElements[0].focus();
flush();
// Focus should have moved to the next editable cell.
expect(document.activeElement).toBe(component.getEditCell(0, 1));
expect(component.lensIsOpen()).toBe(false);
}));
});
});
@@ -26,6 +26,11 @@ import {debounceTime, filter, map, mapTo, startWith, takeUntil} from 'rxjs/opera
import {CELL_SELECTOR, EDIT_PANE_CLASS, EDIT_PANE_SELECTOR, ROW_SELECTOR} from './constants';
import {EditEventDispatcher} from './edit-event-dispatcher';
import {FocusDispatcher} from './focus-dispatcher';
import {
FocusEscapeNotifier,
FocusEscapeNotifierDirection,
FocusEscapeNotifierFactory
} from './focus-escape-notifier';
import {closest} from './polyfill';
import {PopoverEditPositionStrategyFactory} from './popover-edit-position-strategy-factory';
@@ -112,7 +117,7 @@ export class CdkEditable implements AfterViewInit, OnDestroy {
* Makes the cell focusable.
*/
@Directive({
selector: '[cdkPopoverEdit]',
selector: '[cdkPopoverEdit]:not([cdkPopoverEditTabOut])',
host: {
'tabIndex': '0',
'class': 'cdk-popover-edit-cell',
@@ -179,6 +184,14 @@ export class CdkPopoverEdit<C> implements AfterViewInit, OnDestroy {
}
}
protected initFocusTrap(): void {
this.focusTrap = this.focusTrapFactory.create(this.overlayRef!.overlayElement);
}
protected closeEditOverlay(): void {
this.editEventDispatcher.doneEditingCell(this.elementRef.nativeElement!);
}
private _startListeningToEditEvents(): void {
this.editEventDispatcher.editingCell(this.elementRef.nativeElement!)
.pipe(takeUntil(this.destroyed))
@@ -207,12 +220,10 @@ export class CdkPopoverEdit<C> implements AfterViewInit, OnDestroy {
scrollStrategy: this.overlay.scrollStrategies.reposition(),
});
this.focusTrap = this.focusTrapFactory.create(this.overlayRef.overlayElement);
this.initFocusTrap();
this.overlayRef.overlayElement.setAttribute('aria-role', 'dialog');
this.overlayRef.detachments().subscribe(() => {
this.editEventDispatcher.doneEditingCell(this.elementRef.nativeElement!);
});
this.overlayRef.detachments().subscribe(() => this.closeEditOverlay());
}
private _showEditOverlay(): void {
@@ -266,6 +277,63 @@ export class CdkPopoverEdit<C> implements AfterViewInit, OnDestroy {
}
}
/**
* Attaches an ng-template to a cell and shows it when instructed to by the
* EditEventDispatcher service.
* Makes the cell focusable.
*/
@Directive({
selector: '[cdkPopoverEdit] [cdkPopoverEditTabOut]',
host: {
'tabIndex': '0',
'class': 'cdk-popover-edit-cell',
'[attr.aria-haspopup]': 'true',
}
})
export class CdkPopoverEditTabOut<C> extends CdkPopoverEdit<C> {
protected focusTrap?: FocusEscapeNotifier;
constructor(
editEventDispatcher: EditEventDispatcher,
elementRef: ElementRef,
focusTrapFactory: FocusTrapFactory,
ngZone: NgZone,
overlay: Overlay,
positionFactory: PopoverEditPositionStrategyFactory,
scrollDispatcher: ScrollDispatcher,
viewContainerRef: ViewContainerRef,
viewportRuler: ViewportRuler,
protected readonly focusEscapeNotifierFactory: FocusEscapeNotifierFactory,
protected readonly focusDispatcher: FocusDispatcher) {
super(
editEventDispatcher,
elementRef,
focusTrapFactory,
ngZone,
overlay,
positionFactory,
scrollDispatcher,
viewContainerRef,
viewportRuler);
}
protected initFocusTrap(): void {
this.focusTrap = this.focusEscapeNotifierFactory.create(this.overlayRef!.overlayElement);
this.focusTrap.escapes().pipe(takeUntil(this.destroyed)).subscribe(direction => {
if (this.editEventDispatcher.editRef) {
this.editEventDispatcher.editRef.blur();
}
this.focusDispatcher.moveFocusHorizontally(
closest(this.elementRef.nativeElement!, CELL_SELECTOR) as HTMLElement,
direction === FocusEscapeNotifierDirection.START ? -1 : 1);
this.closeEditOverlay();
});
}
}
/**
* A structural directive that shows its contents when the table row containing
* it is hovered.
@@ -0,0 +1,13 @@
.example-table {
width: 100%;
}
.example-table th {
text-align: left;
}
.example-table td,
.example-table th {
min-width: 300px;
width: 25%;
}
@@ -0,0 +1,56 @@
<table editable class="example-table">
<!--
This edit lens is specified outside of the cell and must explicitly declare
its context. It could be reused in multiple cells.
-->
<ng-template #weightEdit let-element>
<div style="background-color: white; width: 100%">
<form #f="ngForm"
cdkEditControl
cdkEditControlClickOutBehavior="submit"
(ngSubmit)="onSubmitWeight(element, f)"
[cdkEditControlPreservedFormValue]="preservedWeightValues.get(element)"
(cdkEditControlPreservedFormValueChange)="preservedWeightValues.set(element, $event)">
<input type="number" [ngModel]="element.weight" name="weight" required>
</form>
</div>
</ng-template>
<tr>
<th> No. </th>
<th> Name </th>
<th> Weight </th>
<th> Symbol </th>
</tr>
<tr *ngFor="let element of elements">
<td> {{element.position}} </td>
<td [cdkPopoverEdit]="nameEdit" cdkPopoverEditTabOut cdkEditOpen>
{{element.name}}
<!-- This edit is defined in the cell and can implicitly access element -->
<ng-template #nameEdit>
<div style="background-color: white; width: 100%">
<form #f="ngForm"
cdkEditControl
cdkEditControlClickOutBehavior="submit"
(ngSubmit)="onSubmitName(element, f)"
[cdkEditControlPreservedFormValue]="preservedNameValues.get(element)"
(cdkEditControlPreservedFormValueChange)="preservedNameValues.set(element, $event)">
<input [ngModel]="element.name" name="name" required>
<br>
<button type="submit">Confirm</button>
</form>
</div>
</ng-template>
</td>
<td [cdkPopoverEdit]="weightEdit" [cdkPopoverEditContext]="element"
cdkPopoverEditTabOut cdkEditOpen>
{{element.weight}}
</td>
<td> {{element.symbol}} </td>
</tr>
</table>
@@ -0,0 +1,59 @@
import {Component} from '@angular/core';
import {NgForm} from '@angular/forms';
export interface PeriodicElement {
name: string;
position: number;
weight: number;
symbol: string;
}
const ELEMENT_DATA: PeriodicElement[] = [
{position: 1, name: 'Hydrogen', weight: 1.0079, symbol: 'H'},
{position: 2, name: 'Helium', weight: 4.0026, symbol: 'He'},
{position: 3, name: 'Lithium', weight: 6.941, symbol: 'Li'},
{position: 4, name: 'Beryllium', weight: 9.0122, symbol: 'Be'},
{position: 5, name: 'Boron', weight: 10.811, symbol: 'B'},
{position: 6, name: 'Carbon', weight: 12.0107, symbol: 'C'},
{position: 7, name: 'Nitrogen', weight: 14.0067, symbol: 'N'},
{position: 8, name: 'Oxygen', weight: 15.9994, symbol: 'O'},
{position: 9, name: 'Fluorine', weight: 18.9984, symbol: 'F'},
{position: 10, name: 'Neon', weight: 20.1797, symbol: 'Ne'},
{position: 11, name: 'Sodium', weight: 22.9897, symbol: 'Na'},
{position: 12, name: 'Magnesium', weight: 24.305, symbol: 'Mg'},
{position: 13, name: 'Aluminum', weight: 26.9815, symbol: 'Al'},
{position: 14, name: 'Silicon', weight: 28.0855, symbol: 'Si'},
{position: 15, name: 'Phosphorus', weight: 30.9738, symbol: 'P'},
{position: 16, name: 'Sulfur', weight: 32.065, symbol: 'S'},
{position: 17, name: 'Chlorine', weight: 35.453, symbol: 'Cl'},
{position: 18, name: 'Argon', weight: 39.948, symbol: 'Ar'},
{position: 19, name: 'Potassium', weight: 39.0983, symbol: 'K'},
{position: 20, name: 'Calcium', weight: 40.078, symbol: 'Ca'},
];
/**
* @title CDK Popover Edit with spreadsheet-like configuration on an HTML data-table
*/
@Component({
selector: 'cdk-popover-edit-tab-out-vanilla-table-example',
styleUrls: ['cdk-popover-edit-tab-out-vanilla-table-example.css'],
templateUrl: 'cdk-popover-edit-tab-out-vanilla-table-example.html',
})
export class CdkPopoverEditTabOutVanillaTableExample {
readonly preservedNameValues = new WeakMap<PeriodicElement, any>();
readonly preservedWeightValues = new WeakMap<PeriodicElement, any>();
readonly elements = ELEMENT_DATA;
onSubmitName(element: PeriodicElement, f: NgForm) {
if (!f.valid) { return; }
element.name = f.value.name;
}
onSubmitWeight(element: PeriodicElement, f: NgForm) {
if (!f.valid) { return; }
element.weight = f.value.weight;
}
}