Skip to content

Commit f1cbaab

Browse files
authored
refactor: Move functions into FieldDropdown. (#8634)
* refactor: Move functions into FieldDropdown. * refactor: Make dropdown field image metrics static. * refactor: Use template literals in FieldDropdown validator.
1 parent 3c91b30 commit f1cbaab

1 file changed

Lines changed: 134 additions & 147 deletions

File tree

core/field_dropdown.ts

Lines changed: 134 additions & 147 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,15 @@ export class FieldDropdown extends Field<string> {
9595
private selectedOption!: MenuOption;
9696
override clickTarget_: SVGElement | null = null;
9797

98+
/**
99+
* The y offset from the top of the field to the top of the image, if an image
100+
* is selected.
101+
*/
102+
protected static IMAGE_Y_OFFSET = 5;
103+
104+
/** The total vertical padding above and below an image. */
105+
protected static IMAGE_Y_PADDING = FieldDropdown.IMAGE_Y_OFFSET * 2;
106+
98107
/**
99108
* @param menuGenerator A non-empty array of options for a dropdown list, or a
100109
* function which generates these options. Also accepts Field.SKIP_SETUP
@@ -128,8 +137,8 @@ export class FieldDropdown extends Field<string> {
128137
if (menuGenerator === Field.SKIP_SETUP) return;
129138

130139
if (Array.isArray(menuGenerator)) {
131-
validateOptions(menuGenerator);
132-
const trimmed = trimOptions(menuGenerator);
140+
this.validateOptions(menuGenerator);
141+
const trimmed = this.trimOptions(menuGenerator);
133142
this.menuGenerator_ = trimmed.options;
134143
this.prefixField = trimmed.prefix || null;
135144
this.suffixField = trimmed.suffix || null;
@@ -401,7 +410,7 @@ export class FieldDropdown extends Field<string> {
401410
if (useCache && this.generatedOptions) return this.generatedOptions;
402411

403412
this.generatedOptions = this.menuGenerator_();
404-
validateOptions(this.generatedOptions);
413+
this.validateOptions(this.generatedOptions);
405414
return this.generatedOptions;
406415
}
407416

@@ -520,7 +529,7 @@ export class FieldDropdown extends Field<string> {
520529
const hasBorder = !!this.borderRect_;
521530
const height = Math.max(
522531
hasBorder ? this.getConstants()!.FIELD_DROPDOWN_BORDER_RECT_HEIGHT : 0,
523-
imageHeight + IMAGE_Y_PADDING,
532+
imageHeight + FieldDropdown.IMAGE_Y_PADDING,
524533
);
525534
const xPadding = hasBorder
526535
? this.getConstants()!.FIELD_BORDER_RECT_X_PADDING
@@ -661,6 +670,127 @@ export class FieldDropdown extends Field<string> {
661670
// override the static fromJson method.
662671
return new this(options.options, undefined, options);
663672
}
673+
674+
/**
675+
* Factor out common words in statically defined options.
676+
* Create prefix and/or suffix labels.
677+
*/
678+
protected trimOptions(options: MenuOption[]): {
679+
options: MenuOption[];
680+
prefix?: string;
681+
suffix?: string;
682+
} {
683+
let hasImages = false;
684+
const trimmedOptions = options.map(([label, value]): MenuOption => {
685+
if (typeof label === 'string') {
686+
return [parsing.replaceMessageReferences(label), value];
687+
}
688+
689+
hasImages = true;
690+
// Copy the image properties so they're not influenced by the original.
691+
// NOTE: No need to deep copy since image properties are only 1 level deep.
692+
const imageLabel =
693+
label.alt !== null
694+
? {...label, alt: parsing.replaceMessageReferences(label.alt)}
695+
: {...label};
696+
return [imageLabel, value];
697+
});
698+
699+
if (hasImages || options.length < 2) return {options: trimmedOptions};
700+
701+
const stringOptions = trimmedOptions as [string, string][];
702+
const stringLabels = stringOptions.map(([label]) => label);
703+
704+
const shortest = utilsString.shortestStringLength(stringLabels);
705+
const prefixLength = utilsString.commonWordPrefix(stringLabels, shortest);
706+
const suffixLength = utilsString.commonWordSuffix(stringLabels, shortest);
707+
708+
if (
709+
(!prefixLength && !suffixLength) ||
710+
shortest <= prefixLength + suffixLength
711+
) {
712+
// One or more strings will entirely vanish if we proceed. Abort.
713+
return {options: stringOptions};
714+
}
715+
716+
const prefix = prefixLength
717+
? stringLabels[0].substring(0, prefixLength - 1)
718+
: undefined;
719+
const suffix = suffixLength
720+
? stringLabels[0].substr(1 - suffixLength)
721+
: undefined;
722+
return {
723+
options: this.applyTrim(stringOptions, prefixLength, suffixLength),
724+
prefix,
725+
suffix,
726+
};
727+
}
728+
729+
/**
730+
* Use the calculated prefix and suffix lengths to trim all of the options in
731+
* the given array.
732+
*
733+
* @param options Array of option tuples:
734+
* (human-readable text or image, language-neutral name).
735+
* @param prefixLength The length of the common prefix.
736+
* @param suffixLength The length of the common suffix
737+
* @returns A new array with all of the option text trimmed.
738+
*/
739+
private applyTrim(
740+
options: [string, string][],
741+
prefixLength: number,
742+
suffixLength: number,
743+
): MenuOption[] {
744+
return options.map(([text, value]) => [
745+
text.substring(prefixLength, text.length - suffixLength),
746+
value,
747+
]);
748+
}
749+
750+
/**
751+
* Validates the data structure to be processed as an options list.
752+
*
753+
* @param options The proposed dropdown options.
754+
* @throws {TypeError} If proposed options are incorrectly structured.
755+
*/
756+
protected validateOptions(options: MenuOption[]) {
757+
if (!Array.isArray(options)) {
758+
throw TypeError('FieldDropdown options must be an array.');
759+
}
760+
if (!options.length) {
761+
throw TypeError('FieldDropdown options must not be an empty array.');
762+
}
763+
let foundError = false;
764+
for (let i = 0; i < options.length; i++) {
765+
const tuple = options[i];
766+
if (!Array.isArray(tuple)) {
767+
foundError = true;
768+
console.error(
769+
`Invalid option[${i}]: Each FieldDropdown option must be an array.
770+
Found: ${tuple}`,
771+
);
772+
} else if (typeof tuple[1] !== 'string') {
773+
foundError = true;
774+
console.error(
775+
`Invalid option[${i}]: Each FieldDropdown option id must be a string.
776+
Found ${tuple[1]} in: ${tuple}`,
777+
);
778+
} else if (
779+
tuple[0] &&
780+
typeof tuple[0] !== 'string' &&
781+
typeof tuple[0].src !== 'string'
782+
) {
783+
foundError = true;
784+
console.error(
785+
`Invalid option[${i}]: Each FieldDropdown option must have a string
786+
label or image description. Found ${tuple[0]} in: ${tuple}`,
787+
);
788+
}
789+
}
790+
if (foundError) {
791+
throw TypeError('Found invalid FieldDropdown options.');
792+
}
793+
}
664794
}
665795

666796
/**
@@ -721,147 +851,4 @@ export interface FieldDropdownFromJsonConfig extends FieldDropdownConfig {
721851
*/
722852
export type FieldDropdownValidator = FieldValidator<string>;
723853

724-
/**
725-
* The y offset from the top of the field to the top of the image, if an image
726-
* is selected.
727-
*/
728-
const IMAGE_Y_OFFSET = 5;
729-
730-
/** The total vertical padding above and below an image. */
731-
const IMAGE_Y_PADDING: number = IMAGE_Y_OFFSET * 2;
732-
733-
/**
734-
* Factor out common words in statically defined options.
735-
* Create prefix and/or suffix labels.
736-
*/
737-
function trimOptions(options: MenuOption[]): {
738-
options: MenuOption[];
739-
prefix?: string;
740-
suffix?: string;
741-
} {
742-
let hasImages = false;
743-
const trimmedOptions = options.map(([label, value]): MenuOption => {
744-
if (typeof label === 'string') {
745-
return [parsing.replaceMessageReferences(label), value];
746-
}
747-
748-
hasImages = true;
749-
// Copy the image properties so they're not influenced by the original.
750-
// NOTE: No need to deep copy since image properties are only 1 level deep.
751-
const imageLabel =
752-
label.alt !== null
753-
? {...label, alt: parsing.replaceMessageReferences(label.alt)}
754-
: {...label};
755-
return [imageLabel, value];
756-
});
757-
758-
if (hasImages || options.length < 2) return {options: trimmedOptions};
759-
760-
const stringOptions = trimmedOptions as [string, string][];
761-
const stringLabels = stringOptions.map(([label]) => label);
762-
763-
const shortest = utilsString.shortestStringLength(stringLabels);
764-
const prefixLength = utilsString.commonWordPrefix(stringLabels, shortest);
765-
const suffixLength = utilsString.commonWordSuffix(stringLabels, shortest);
766-
767-
if (
768-
(!prefixLength && !suffixLength) ||
769-
shortest <= prefixLength + suffixLength
770-
) {
771-
// One or more strings will entirely vanish if we proceed. Abort.
772-
return {options: stringOptions};
773-
}
774-
775-
const prefix = prefixLength
776-
? stringLabels[0].substring(0, prefixLength - 1)
777-
: undefined;
778-
const suffix = suffixLength
779-
? stringLabels[0].substr(1 - suffixLength)
780-
: undefined;
781-
return {
782-
options: applyTrim(stringOptions, prefixLength, suffixLength),
783-
prefix,
784-
suffix,
785-
};
786-
}
787-
788-
/**
789-
* Use the calculated prefix and suffix lengths to trim all of the options in
790-
* the given array.
791-
*
792-
* @param options Array of option tuples:
793-
* (human-readable text or image, language-neutral name).
794-
* @param prefixLength The length of the common prefix.
795-
* @param suffixLength The length of the common suffix
796-
* @returns A new array with all of the option text trimmed.
797-
*/
798-
function applyTrim(
799-
options: [string, string][],
800-
prefixLength: number,
801-
suffixLength: number,
802-
): MenuOption[] {
803-
return options.map(([text, value]) => [
804-
text.substring(prefixLength, text.length - suffixLength),
805-
value,
806-
]);
807-
}
808-
809-
/**
810-
* Validates the data structure to be processed as an options list.
811-
*
812-
* @param options The proposed dropdown options.
813-
* @throws {TypeError} If proposed options are incorrectly structured.
814-
*/
815-
function validateOptions(options: MenuOption[]) {
816-
if (!Array.isArray(options)) {
817-
throw TypeError('FieldDropdown options must be an array.');
818-
}
819-
if (!options.length) {
820-
throw TypeError('FieldDropdown options must not be an empty array.');
821-
}
822-
let foundError = false;
823-
for (let i = 0; i < options.length; i++) {
824-
const tuple = options[i];
825-
if (!Array.isArray(tuple)) {
826-
foundError = true;
827-
console.error(
828-
'Invalid option[' +
829-
i +
830-
']: Each FieldDropdown option must be an ' +
831-
'array. Found: ',
832-
tuple,
833-
);
834-
} else if (typeof tuple[1] !== 'string') {
835-
foundError = true;
836-
console.error(
837-
'Invalid option[' +
838-
i +
839-
']: Each FieldDropdown option id must be ' +
840-
'a string. Found ' +
841-
tuple[1] +
842-
' in: ',
843-
tuple,
844-
);
845-
} else if (
846-
tuple[0] &&
847-
typeof tuple[0] !== 'string' &&
848-
typeof tuple[0].src !== 'string'
849-
) {
850-
foundError = true;
851-
console.error(
852-
'Invalid option[' +
853-
i +
854-
']: Each FieldDropdown option must have a ' +
855-
'string label or image description. Found' +
856-
tuple[0] +
857-
' in: ',
858-
tuple,
859-
);
860-
}
861-
}
862-
if (foundError) {
863-
throw TypeError('Found invalid FieldDropdown options.');
864-
}
865-
}
866-
867854
fieldRegistry.register('field_dropdown', FieldDropdown);

0 commit comments

Comments
 (0)