customizer-validate-wcag-color-contrast.js 5.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213
  1. /**
  2. * @author Per Soderlind
  3. * @link https://github.com/soderlind/2016-customizer-demo
  4. * global wp, seedletValidateWCAGColorContrastExports
  5. * global seedletValidateContrastText
  6. * exported validateWCAGColorContrast
  7. **/
  8. ( function( $, api, exports ) {
  9. var self = {
  10. validate_color_contrast: []
  11. };
  12. if ( exports ) {
  13. $.extend( self, exports );
  14. }
  15. var code = 'contrast_warning';
  16. /**
  17. * Add contrast validation to a control if it is entitled (is a valid color control).
  18. *
  19. * @param {wp.customize.Control} setting - Control.
  20. * @param {wp.customize.Value} setting.validationMessage - Validation message.
  21. * @return {boolean} Whether validation was added.
  22. */
  23. self.addWCAGColorContrastValidation = function( setting ) {
  24. var initialValidate;
  25. if ( ! self.isColorControl( setting ) ) {
  26. return false;
  27. }
  28. initialValidate = setting.validate;
  29. /**
  30. * Wrap the setting's validate() method to do validation on the value to be sent to the server.
  31. *
  32. * @param {mixed} value - New value being assigned to the setting.
  33. * @returns {*}
  34. */
  35. setting.validate = function (value){
  36. var failsWCAG = false;
  37. _.each ( self.validate_color_contrast[setting.id], function( color_to_compare_id ){
  38. var color_to_compare = $( '#customize-control-' + color_to_compare_id + ' .wp-color-result')[0].style.backgroundColor.match(/\d+/g);
  39. var contrast = self.rgb( [ parseInt(color_to_compare[0]), parseInt(color_to_compare[1]), parseInt(color_to_compare[2]) ], self.hexRgb( value ) );
  40. var score = self.score( contrast );
  41. if ( score === 'Fail' ){
  42. failsWCAG = true;
  43. }
  44. });
  45. if ( failsWCAG ){
  46. var validationWarning = new api.Notification( code, { message: seedletValidateContrastText, type: 'warning' } );
  47. setTimeout( function(){
  48. self.custom_colors_setting.notifications.add( code, validationWarning );
  49. }, 400);
  50. } else {
  51. setTimeout( function(){
  52. self.custom_colors_setting.notifications.remove( code );
  53. }, 400);
  54. }
  55. return value;
  56. }
  57. };
  58. /**
  59. * Return whether the setting is entitled (i.e. if it is a title or has a title).
  60. *
  61. * @param {wp.customize.Setting} setting - Setting.
  62. * @returns {boolean}
  63. */
  64. self.isColorControl = function( setting ) {
  65. return _.findKey( self.validate_color_contrast, function( key, value ) {
  66. return value == setting.id;
  67. } );
  68. };
  69. api.bind( 'add', function( setting ) {
  70. self.addWCAGColorContrastValidation( setting );
  71. } );
  72. api( 'custom_colors_active', function( setting ) {
  73. self.custom_colors_setting = setting;
  74. setting.validate = function( value ) {
  75. if ( value == 'default' ){
  76. setting.notifications.remove( code );
  77. }
  78. return value;
  79. }
  80. });
  81. self.sprintf = function( format ) {
  82. for( var i=1; i < arguments.length; i++ ) {
  83. format = format.replace( /%s/, arguments[i] );
  84. }
  85. return format;
  86. };
  87. /**
  88. * Methods used to calculate WCAG Color Contrast
  89. */
  90. // from https://github.com/sindresorhus/hex-rgb
  91. self.hexRgb = function (hex) {
  92. if (typeof hex !== 'string') {
  93. throw new TypeError('Expected a string');
  94. }
  95. hex = hex.replace(/^#/, '');
  96. if (hex.length === 3) {
  97. hex = hex[0] + hex[0] + hex[1] + hex[1] + hex[2] + hex[2];
  98. }
  99. var num = parseInt(hex, 16);
  100. return [num >> 16, num >> 8 & 255, num & 255];
  101. };
  102. // from https://github.com/tmcw/relative-luminance
  103. // # Relative luminance
  104. //
  105. // http://www.w3.org/TR/2008/REC-WCAG20-20081211/#relativeluminancedef
  106. // https://en.wikipedia.org/wiki/Luminance_(relative)
  107. // https://en.wikipedia.org/wiki/Luminosity_function
  108. // https://en.wikipedia.org/wiki/Rec._709#Luma_coefficients
  109. // red, green, and blue coefficients
  110. var rc = 0.2126,
  111. gc = 0.7152,
  112. bc = 0.0722,
  113. // low-gamma adjust coefficient
  114. lowc = 1 / 12.92;
  115. self.adjustGamma = function( g ) {
  116. return Math.pow((g + 0.055) / 1.055, 2.4);
  117. };
  118. /**
  119. * Given a 3-element array of R, G, B varying from 0 to 255, return the luminance
  120. * as a number from 0 to 1.
  121. * @param {Array<number>} rgb 3-element array of a color
  122. * @returns {number} luminance, between 0 and 1
  123. * @example
  124. * var luminance = require('relative-luminance');
  125. * var black_lum = luminance([0, 0, 0]); // 0
  126. */
  127. self.relativeLuminance = function (rgb) {
  128. var rsrgb = rgb[0] / 255;
  129. var gsrgb = rgb[1] / 255;
  130. var bsrgb = rgb[2] / 255;
  131. var r = rsrgb <= 0.03928 ? rsrgb * lowc : self.adjustGamma(rsrgb),
  132. g = gsrgb <= 0.03928 ? gsrgb * lowc : self.adjustGamma(gsrgb),
  133. b = bsrgb <= 0.03928 ? bsrgb * lowc : self.adjustGamma(bsrgb);
  134. return r * rc + g * gc + b * bc;
  135. };
  136. // from https://github.com/tmcw/wcag-contrast
  137. /**
  138. * Get the contrast ratio between two relative luminance values
  139. * @param {number} a luminance value
  140. * @param {number} b luminance value
  141. * @returns {number} contrast ratio
  142. * @example
  143. * luminance(1, 1); // = 1
  144. */
  145. self.luminance = function(a, b) {
  146. var l1 = Math.max(a, b);
  147. var l2 = Math.min(a, b);
  148. return (l1 + 0.05) / (l2 + 0.05);
  149. };
  150. /**
  151. * Get a score for the contrast between two colors as rgb triplets
  152. * @param {array} a
  153. * @param {array} b
  154. * @returns {number} contrast ratio
  155. * @example
  156. * rgb([0, 0, 0], [255, 255, 255]); // = 21
  157. */
  158. self.rgb = function(a, b) {
  159. return self.luminance(self.relativeLuminance(a), self.relativeLuminance(b));
  160. };
  161. /**
  162. * Get a score for the contrast between two colors as hex strings
  163. * @param {string} a hex value
  164. * @param {string} b hex value
  165. * @returns {number} contrast ratio
  166. * @example
  167. * hex('#000', '#fff'); // = 21
  168. */
  169. self.hex = function(a, b) {
  170. return self.rgb(self.hexRgb(a), self.hexRgb(b));
  171. };
  172. /**
  173. * Get a textual score from a numeric contrast value
  174. * @param {number} contrast
  175. * @returns {string} score
  176. * @example
  177. * score(10); // = 'AAA'
  178. */
  179. self.score = function(contrast) {
  180. return contrast >= 7 ? "AAA" : contrast >= 4.5 ? "AA" : "Fail";
  181. };
  182. return self;
  183. } )( jQuery, wp.customize, seedletValidateWCAGColorContrastExports );