@@ -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 */
722852export 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-
867854fieldRegistry . register ( 'field_dropdown' , FieldDropdown ) ;
0 commit comments