├── a11y_styles.css ├── a11y_tests.js ├── config ├── additional_tests_meta.php └── ncsu_defaults.php ├── icon.png ├── inc ├── info_metabox.php ├── misc.php ├── options.php └── publish_metabox.php ├── ncsu-a11y-helper.php ├── readme.txt └── vendor ├── RationalOptionPages └── RationalOptionPages.php └── a11y-dialog ├── LICENSE └── a11y-dialog.min.js /a11y_styles.css: -------------------------------------------------------------------------------- 1 | .a11y-report-generator button { 2 | background: none; 3 | border: none; 4 | color: #eee; 5 | padding: 0 8px 0 7px !important; 6 | } 7 | 8 | .a11y-sr-text { 9 | position: absolute; 10 | width: 1px; 11 | height: 1px; 12 | padding: 0; 13 | overflow: hidden; 14 | clip: rect(0, 0, 0, 0); 15 | white-space: nowrap; 16 | -webkit-clip-path: inset(50%); 17 | clip-path: inset(50%); 18 | border: 0; 19 | } 20 | 21 | .a11y-dialog { 22 | font-family: arial, sans-serif; 23 | } 24 | 25 | /* -------------------------------------------------------------------------- *\ 26 | * Necessary styling for the dialog to work 27 | * -------------------------------------------------------------------------- */ 28 | 29 | .a11y-dialog[aria-hidden="true"] { 30 | display: none; 31 | } 32 | 33 | /* -------------------------------------------------------------------------- *\ 34 | * Styling to make the dialog look like a dialog 35 | * -------------------------------------------------------------------------- */ 36 | 37 | .a11y-dialog-overlay { 38 | z-index: 99; 39 | background-color: rgba(0, 0, 0, 0.66); 40 | position: fixed; 41 | top: 0; 42 | left: 0; 43 | bottom: 0; 44 | right: 0; 45 | } 46 | 47 | .a11y-dialog-content { 48 | background-color: rgb(255, 255, 255); 49 | z-index: 100; 50 | position: fixed; 51 | top: 50%; 52 | left: 50%; 53 | -webkit-transform: translate(-50%, -50%); 54 | -ms-transform: translate(-50%, -50%); 55 | transform: translate(-50%, -50%); 56 | } 57 | 58 | .a11y-dialog-content p.a11y-summary { 59 | white-space: pre-line; 60 | } 61 | 62 | /* -------------------------------------------------------------------------- *\ 63 | * Extra dialog styling to make it shiny 64 | * -------------------------------------------------------------------------- */ 65 | 66 | .a11y-dialog a { 67 | text-decoration: underline; 68 | } 69 | 70 | .a11y-dialog-content { 71 | padding: 1em; 72 | max-width: 90%; 73 | width: 1200px; 74 | max-height: 90%; 75 | border-radius: 2px; 76 | overflow-y: scroll; 77 | } 78 | 79 | @media screen and (min-width: 980px) { 80 | .a11y-test-content { 81 | display: grid; 82 | grid-template-columns: 50% 50%; 83 | } 84 | } 85 | 86 | #a11y-report-content { 87 | padding: 1em 0; 88 | font-size: 0.9em; 89 | } 90 | 91 | #a11y-report-content li:before { 92 | display: none; 93 | } 94 | 95 | #a11y-report-content li.a11y-report-entry { 96 | list-style: none; 97 | padding: 1em; 98 | } 99 | 100 | #a11y-report-content li.a11y-report-entry:nth-child(even) { 101 | background-color: #f2f2f2; 102 | } 103 | 104 | .a11y-test-info, .a11y-test-html { 105 | padding: 2rem; 106 | } 107 | 108 | .a11y-html-code { 109 | background: #f2f2f2; 110 | padding: 1.5rem; 111 | } 112 | 113 | .a11y-html-code code { 114 | background-color: #f2f2f2; 115 | font-family: monospace; 116 | white-space: normal; 117 | } 118 | 119 | @media screen and (min-width: 700px) { 120 | .a11y-dialog-content { 121 | padding: 2em; 122 | } 123 | 124 | } 125 | 126 | .a11y-dialog-overlay { 127 | background-color: rgba(43, 46, 56, 0.9); 128 | } 129 | 130 | .a11y-dialog h1 { 131 | margin: 0; 132 | font-size: 1.25em; 133 | } 134 | 135 | .a11y-dialog h2 { 136 | margin: 1rem 0 0.5rem 0; 137 | font-size: 1em; 138 | font-weight: bold; 139 | text-transform: uppercase; 140 | } 141 | 142 | .a11y-dialog p { 143 | margin: 0 0 1rem 0; 144 | } 145 | 146 | .a11y-dialog-close { 147 | position: absolute; 148 | top: 0.5em; 149 | right: 0.5em; 150 | border: 0; 151 | padding: 0.5em; 152 | background-color: #cc0000; 153 | font-weight: bold; 154 | font-size: 1.25em; 155 | line-height: 1em; 156 | width: auto; 157 | height: auto; 158 | text-align: center; 159 | cursor: pointer; 160 | transition: 0.15s; 161 | color: #fff; 162 | } 163 | 164 | .a11y-dialog-close:hover, .a11y-dialog-close:focus, .a11y-dialog-close:active { 165 | background-color: #990000; 166 | } 167 | 168 | @media screen and (min-width: 700px) { 169 | .a11y-dialog-close { 170 | top: 1em; 171 | right: 1em; 172 | } 173 | } 174 | 175 | .a11y-dialog-footer { 176 | font-size: 0.8em; 177 | } 178 | 179 | /* Annotated Preview */ 180 | 181 | .a11y-more-button { 182 | font-size: 1em; 183 | text-transform: uppercase; 184 | padding: 0.5rem 1rem; 185 | background-color: #427E93; 186 | color: #fff; 187 | width: 100%; 188 | margin: 0.5rem 0; 189 | border: none; 190 | } 191 | 192 | .a11y-more-button:hover, .a11y-more-button:focus, .a11y-more-button:active { 193 | background-color: #326070; 194 | } 195 | 196 | .a11y-indicator { 197 | display: inline-block; 198 | width: 1em !important; 199 | height: 1em !important; 200 | border-radius: 50%; 201 | margin: 0 0.5rem 0 0 !important; 202 | } 203 | 204 | /* Info: Indicates additional best practice not tested by aXe that we want to draw attention to */ 205 | .a11y-info { 206 | outline: 0.25em solid steelblue; 207 | display: inline-block; 208 | } 209 | 210 | .a11y-info-indicator { 211 | background-color: steelblue; 212 | } 213 | 214 | .a11y-info .a11y-annotation { 215 | z-index: 0; 216 | } 217 | 218 | .a11y-minor { 219 | outline: 0.25em solid gold; 220 | display: inline-block; 221 | } 222 | 223 | .a11y-minor-indicator { 224 | background-color: gold; 225 | } 226 | 227 | .a11y-minor .a11y-annotation { 228 | z-index: 10; 229 | } 230 | 231 | .a11y-moderate, .a11y-null { 232 | outline: 0.25em solid orange; 233 | display: inline-block; 234 | } 235 | 236 | .a11y-moderate-indicator, .a11y-null-indicator { 237 | background-color: orange; 238 | } 239 | 240 | .a11y-moderate .a11y-annotation { 241 | z-index: 20; 242 | } 243 | 244 | .a11y-serious { 245 | outline: 0.25em solid coral; 246 | display: inline-block; 247 | } 248 | 249 | .a11y-serious-indicator { 250 | background-color: coral; 251 | } 252 | 253 | .a11y-serious .a11y-annotation { 254 | z-index: 30; 255 | } 256 | 257 | .a11y-critical { 258 | outline: 0.25em solid red; 259 | display: inline-block; 260 | } 261 | 262 | .a11y-critical-indicator { 263 | background-color: red; 264 | } 265 | 266 | .a11y-critical .a11y-annotation { 267 | z-index: 40; 268 | } 269 | 270 | .a11y-issue { 271 | padding: 0.25em; 272 | position: relative; 273 | } 274 | 275 | .a11y-annotation:focus-within, .a11y-annotation:hover { 276 | z-index: 99; 277 | } 278 | 279 | /*.a11y-issue:hover, .a11y-issue:focus, .a11y-issue:active { 280 | background-color: yellow !important; 281 | }*/ 282 | 283 | .a11y-annotation { 284 | position: absolute; 285 | left: 50%; 286 | margin-left: -8.5em; 287 | /*top: -8.5rem;*/ 288 | bottom: 100%; 289 | margin-bottom: 0.5rem; 290 | display: block; 291 | /*max-width: 50%;*/ 292 | width: 18em; 293 | height: auto; 294 | /*border-radius: 0.25rem; */ 295 | padding: 0.5em; 296 | /* color: #333; 297 | text-decoration: none; */ 298 | background-color: #fff; 299 | font-size: 1em; 300 | font-weight: 400; 301 | line-height: 1.3em; 302 | text-decoration: none; 303 | /*box-shadow: 0px 0px 5px #666;*/ 304 | border: solid 1px #ccc; 305 | background: #333; 306 | color: #fff; 307 | font-family: arial, sans-serif; 308 | letter-spacing: 1px; 309 | } 310 | 311 | .a11y-annotation:before { 312 | border-top: 1em solid #333; 313 | border-left: 1em solid transparent; 314 | border-right: 1em solid transparent; 315 | content: ''; 316 | right: 8em; 317 | bottom: -1em; 318 | position: absolute; 319 | /* transform: translateX(-50%); 320 | -wekbkit-transform: translateX(-50%); 321 | -moz-transform: translateX(-50%); 322 | -ms-transform: translateX(-50%); 323 | -o-transform: translateX(-50%);*/ 324 | } 325 | 326 | .a11y-impact { 327 | text-transform: capitalize; 328 | } 329 | 330 | .a11y-annotation .a11y-help { 331 | display: block; 332 | width: 90% !important; 333 | margin-left: 1.5em; 334 | } 335 | 336 | /*.a11y-annotation:hover, .a11y-annotation:focus, .a11y-annotation:active, .a11y-issue:hover .a11y-annotation { 337 | background-color: yellow; 338 | color: #000; 339 | z-index: 1000; 340 | box-shadow: 0px 0px 5px #ccc; 341 | }*/ 342 | 343 | .a11y-annotation .a11y-annotation { 344 | margin-top: 3em; 345 | } -------------------------------------------------------------------------------- /a11y_tests.js: -------------------------------------------------------------------------------- 1 | // Accessibility Tests 2 | 3 | (function($) { 4 | 5 | // For HTML escaping... 6 | 7 | var entityMap = { 8 | '&': '&', 9 | '<': '<', 10 | '>': '>', 11 | '"': '"', 12 | "'": ''', 13 | '/': '/', 14 | '`': '`', 15 | '=': '=' 16 | }; 17 | 18 | function escapeHtml(string) { 19 | return String(string).replace(/[&<>"'`=\/]/g, function (s) { 20 | return entityMap[s]; 21 | }); 22 | } 23 | 24 | jQuery.fn.extend({ 25 | getPath: function() { 26 | var pathes = []; 27 | 28 | 29 | // domain.each(function(index, element) { 30 | this.each(function(index, element) { 31 | var path, $node = jQuery(element); 32 | 33 | var domain = $(this).parentsUntil( $(custom_options.wrapper) ); 34 | 35 | // for (domain[i]) { 36 | while ($node.length) { 37 | // while ( $node.parentsUntil( $(custom_options.wrapper) ) ) { 38 | 39 | var realNode = $node.get(0), name = realNode.localName; 40 | if (!name) { break; } 41 | 42 | name = name.toLowerCase(); 43 | var parent = $node.parent(); 44 | var sameTagSiblings = parent.children(name); 45 | 46 | if (sameTagSiblings.length > 1) 47 | { 48 | allSiblings = parent.children(); 49 | var index = allSiblings.index(realNode) +1; 50 | if (index > 0) { 51 | name += ':nth-child(' + index + ')'; 52 | } 53 | } 54 | 55 | path = name + (path ? ' > ' + path : ''); 56 | $node = parent; 57 | 58 | i++; 59 | } 60 | 61 | pathes.push(path); 62 | }); 63 | 64 | return pathes.join(','); 65 | } 66 | }); 67 | 68 | // Options and custom configuration stored under the custom_options object 69 | 70 | // Default messages for our additional, non-aXe tests 71 | var my_additional_tests = {}; 72 | my_additional_tests = JSON.parse(custom_options.additional_tests_meta); 73 | 74 | // Plugin comes packaged with a custom "ncsu defaults" config file to enable/disable certain tests and change the messages displayed to users. Site admins can upload a custom config file to override those defaults. Either way, it gets passed to the JS right here. 75 | var customize_tests = {}; 76 | 77 | if ( custom_options.config ) { 78 | customize_tests = custom_options.config; 79 | } else if ( custom_options.ncsu_defaults ) { 80 | customize_tests = JSON.parse(custom_options.ncsu_defaults); 81 | } 82 | 83 | // aXe Context: We're only going to be testing the contents of the wrapper element specified for this post type/page template in the plugin settings 84 | var context = { 85 | include: [custom_options.wrapper] 86 | }; 87 | 88 | // aXe Options: Custom enable/disable of tests 89 | var options = { 90 | "rules": {} 91 | }; 92 | 93 | // If a custom config file enables or disables tests, add that to the options 94 | $.each(customize_tests, function(test, attribute) { 95 | $.each(attribute, function(key, value) { 96 | if ( key == 'enabled' ) { 97 | options.rules[test] = { 'enabled' : value }; 98 | } 99 | }); 100 | }); 101 | 102 | // Run aXe 103 | axe.run(context, options, generate_annotated_preview); 104 | 105 | // Run additional custom tests 106 | function additional_tests() { 107 | 108 | var i = 0; 109 | 110 | var failed_tests = {}; 111 | 112 | // Heading Tests 113 | // - Did you skip a heading level? 114 | // - Do you have extra h1's? 115 | // - Do you have a lot of text in a heading? 116 | // - Are you using ... as a heading? 117 | 118 | // Empty variable for comparing against the previous heading 119 | var previous = null; 120 | 121 | $(custom_options.wrapper).find('h1,h2,h3,h4,h5,h6').each( 122 | function(){ 123 | 124 | var depthcurrent = parseInt(this.tagName.substring(1)); 125 | 126 | // Check for skipped heading levels 127 | if ( previous ) { 128 | 129 | var diff = depthcurrent - previous; 130 | 131 | if ( diff > 1 ) { 132 | 133 | var test = 'ncsu_skipped_heading'; 134 | var test_msg = $.extend(true, {}, my_additional_tests[test]); 135 | 136 | $(this).addClass(test + '-' + i); 137 | failed_tests[i] = test_msg; 138 | failed_tests[i]['nodes'][0]['target'][0] = '.' + test + '-' + i; 139 | failed_tests[i]['nodes'][0]['html'] = $(this)[0]['outerHTML']; 140 | i = i + 1; 141 | 142 | } 143 | 144 | } 145 | 146 | // Make current depth previous for the next heading in the each loop 147 | previous = depthcurrent; 148 | 149 | var depthcurrent = parseInt(this.tagName.substring(1)); 150 | 151 | // Are you using h1 headings in your post or page? 152 | if ( depthcurrent == 1 && !$(this).hasClass('entry-title') && $(this) != $('h1:first-of-type') ) { 153 | 154 | var test = 'ncsu_multiple_h1'; 155 | var test_msg = $.extend(true, {}, my_additional_tests[test]); 156 | 157 | $(this).addClass(test + '-' + i); 158 | failed_tests[i] = test_msg; 159 | failed_tests[i]['nodes'][0]['target'][0] = '.' + test + '-' + i; 160 | failed_tests[i]['nodes'][0]['html'] = $(this)[0]['outerHTML']; 161 | i = i + 1; 162 | 163 | } 164 | 165 | } 166 | 167 | ); 168 | 169 | // Check each image 170 | // Does the image have an alt attribute? (Covered by aXe test) 171 | // Is the alt attribute empty? 172 | // Does the alt attribute exactly match the image file name? 173 | 174 | $(custom_options.wrapper).find('img').each( 175 | function(){ 176 | 177 | if ( $(this)[0].hasAttribute('alt') ) { 178 | var imgalt = $(this).attr('alt'); 179 | var imgsrc = $(this).attr('src'); 180 | var imgfile = imgsrc.split('/').pop(); 181 | var re = /(?:\.([^.]+))?$/; 182 | var extension = re.exec(imgfile)[0]; 183 | var imgfilename = imgfile.replace(extension, ''); 184 | 185 | if ( imgalt == false || imgalt == typeof undefined ) { 186 | // Empty alt attribute 187 | 188 | var test = 'ncsu_empty_alt'; 189 | var test_msg = $.extend(true, {}, my_additional_tests[test]); 190 | 191 | $(this).addClass(test + '-' + i); 192 | failed_tests[i] = test_msg; 193 | failed_tests[i]['nodes'][0]['target'][0] = '.' + test + '-' + i; 194 | failed_tests[i]['nodes'][0]['html'] = $(this)[0]['outerHTML']; 195 | i = i + 1; 196 | 197 | } else if ( imgalt ) { 198 | // Alt text reminder 199 | var test = 'ncsu_reminder_alt'; 200 | var test_msg = $.extend(true, {}, my_additional_tests[test]); 201 | 202 | $(this).addClass(test + '-' + i); 203 | failed_tests[i] = test_msg; 204 | failed_tests[i]['nodes'][0]['target'][0] = '.' + test + '-' + i; 205 | failed_tests[i]['nodes'][0]['html'] = $(this)[0]['outerHTML']; 206 | i = i + 1; 207 | } 208 | 209 | } 210 | 211 | } 212 | ); 213 | 214 | // Check each table 215 | // Reminder: Tables shouldn't be used for layout 216 | 217 | $(custom_options.wrapper).find('table').each( 218 | function(){ 219 | 220 | // Table usage reminder 221 | 222 | var test = 'ncsu_reminder_table'; 223 | var test_msg = $.extend(true, {}, my_additional_tests[test]); 224 | 225 | $(this).addClass(test + '-' + i); 226 | failed_tests[i] = test_msg; 227 | failed_tests[i]['nodes'][0]['target'][0] = '.' + test + '-' + i; 228 | failed_tests[i]['nodes'][0]['html'] = $(this)[0]['outerHTML']; 229 | i = i + 1; 230 | } 231 | ); 232 | 233 | // Check for ReCAPTCHAs 234 | // Reminder: Don't use CAPTCHAs 235 | 236 | $(custom_options.wrapper).find('iframe').each( 237 | function(){ 238 | 239 | var iframe_src = $(this).attr('src'); 240 | var recaptcha_src = 'https://www.google.com/recaptcha/'; 241 | 242 | // ReCAPTCHA 243 | 244 | if(iframe_src.indexOf(recaptcha_src) != -1){ 245 | 246 | var test = 'ncsu_captcha'; 247 | var test_msg = $.extend(true, {}, my_additional_tests[test]); 248 | 249 | $(this).addClass(test + '-' + i); 250 | failed_tests[i] = test_msg; 251 | failed_tests[i]['nodes'][0]['target'][0] = '.' + test + '-' + i; 252 | failed_tests[i]['nodes'][0]['html'] = $(this)[0]['outerHTML']; 253 | i = i + 1; 254 | 255 | } 256 | 257 | } 258 | ); 259 | 260 | // Check for Really Simple CAPTCHA 261 | // Reminder: Don't use CAPTCHAs 262 | 263 | $(custom_options.wrapper).find('img').each( 264 | function(){ 265 | 266 | var img_alt = $(this).attr('alt'); 267 | 268 | // Really Simple CAPTCHA 269 | 270 | if(img_alt == 'captcha'){ 271 | 272 | var test = 'ncsu_captcha'; 273 | var test_msg = $.extend(true, {}, my_additional_tests[test]); 274 | 275 | $(this).addClass(test + '-' + i); 276 | failed_tests[i] = test_msg; 277 | failed_tests[i]['nodes'][0]['target'][0] = '.' + test + '-' + i; 278 | failed_tests[i]['nodes'][0]['html'] = $(this)[0]['outerHTML']; 279 | i = i + 1; 280 | 281 | } 282 | 283 | } 284 | ); 285 | 286 | // Check for common ambiguous links 287 | 288 | $(custom_options.wrapper).find('a').each( 289 | function(){ 290 | 291 | var link_contents = $(this).text().toUpperCase(); 292 | var here = 'here'.toUpperCase(); 293 | var click = 'click'.toUpperCase(); 294 | var more = 'more'.toUpperCase(); 295 | var download = 'download'.toUpperCase(); 296 | var read = 'read'.toUpperCase(); 297 | var ambiguous_text_detected = 0; 298 | 299 | if(link_contents.indexOf(here) != -1 || link_contents.indexOf(click) != -1 || link_contents.indexOf(more) != -1 || link_contents.indexOf(download) != -1 || link_contents.indexOf(read) != -1){ 300 | ambiguous_text_detected = 1; 301 | } 302 | 303 | if(ambiguous_text_detected == 1){ 304 | var test = 'ncsu_ambiguous_link'; 305 | var test_msg = $.extend(true, {}, my_additional_tests[test]); 306 | 307 | $(this).addClass(test + '-' + i); 308 | failed_tests[i] = test_msg; 309 | failed_tests[i]['nodes'][0]['target'][0] = '.' + test + '-' + i; 310 | failed_tests[i]['nodes'][0]['html'] = $(this)[0]['outerHTML']; 311 | i = i + 1; 312 | } 313 | } 314 | ); 315 | 316 | return failed_tests; 317 | } 318 | 319 | var additional_test_violations = additional_tests(); 320 | 321 | 322 | 323 | // Generate the annotated preview 324 | function generate_annotated_preview(err, results) { 325 | if (err) throw err; 326 | 327 | // Merge incompletes into the violations array 328 | var violations = $.merge(results['violations'], results['incomplete']); 329 | violations = $.merge(violations, additional_test_violations); 330 | 331 | // console.log(violations); 332 | 333 | // Add summary report to the end of the wrapper 334 | var report = `
`; 355 | 356 | $(custom_options.wrapper).append(report); 357 | 358 | // Add button in admin toolbar to open report modal 359 | var generate_button = '` + escapeHtml(html) + `
448 | ' . __( 'Web accessibility', 'ncsu-a11y-helper' ) . ' ' 23 | . __( 'is about ensuring that everyone who visits your website will be able to understand and interact with your website. This especially includes users with disabilities, but good accessibility has benefits to all users. Learn more about ', 'ncsu-a11y-helper' ) . '' . __( 'the basics of web accessibility', 'ncsu-a11y-helper' ) . ', ' . __( 'Inclusive Design Principles', 'ncsu-a11y-helper' ) . ', ' . __( 'and ', 'ncsu-a11y-helper' ) . '' . __( 'Universal Design Principles', 'ncsu-a11y-helper' ) . '.
' 24 | . '' . __( 'Before publishing new content, run the Accessibility Check:', 'ncsu-a11y-helper' ) . '
' 26 | . '' . __( 'Run Accessibility Check', 'ncsu-a11y-helper' ) . ' ' . __( '(opens in a new window)', 'ncsu-a11y-helper' ) . '' 27 | . '' . __( 'The Accessibility Check will scan your post or page for common accessibility issues. If it detects something that might be an issue, it will highlight that part of your post or page, and provide more information about what the issue is, why it matters, and how you can fix it.', 'ncsu-a11y-helper' ) . '
' 28 | . '' . __( 'But automated testing can\'t detect everything. As you write and build your post or page, think about potential barriers that would prevent some users from understanding your content. Some easy manual tests you can do include:', 'ncsu-a11y-helper' ) . '
' 29 | . '' . __( 'Choose which post types should have the Accessibility Helper meta box and "Accessibility Check" button to open the annotated preview. We recommend all post types be included, but there may be some special cases where it doesn\'t make sense.', 'ncsu-a11y-helper' ) . '
', 59 | 'fields' => array( 60 | 'post-types' => array( 61 | 'title' => __( 'Post Types', 'ncsu-a11y-helper' ), 62 | 'type' => 'checkbox', 63 | 'choices' => $post_types_choices, 64 | ), 65 | ), 66 | ), 67 | 'wrapper-element' => array( 68 | 'title' => __( 'Wrapper Elements', 'ncsu-a11y-helper' ), 69 | 'text' => '' . __( 'For each post type (and for each page template), enter the element ID or class that contains your post content. We use WordPress post type classes by default, but some themes may require different IDs or classes. See the ', 'ncsu-a11y-helper' ) . '' . __( 'NC State Accessibility Helper documentation', 'ncsu-a11y-helper' ) . ' ' . __( 'for an example and more details.', 'ncsu-a11y-helper' ) . '
', 70 | 'fields' => $wrapper_elements, 71 | ), 72 | 'custom-config' => array( 73 | 'title' => __( 'Custom Configuration File', 'ncsu-a11y-helper' ), 74 | 'text' => '' . __( 'You can customize the messages your users see, learning resource links, and enable/disable individual accessibility tests through a .txt configuration file. See the ', 'ncsu-a11y-helper' ) . '' . __( 'NC State Accessibility Helper documentation', 'ncsu-a11y-helper' ) . ' ' . __( 'for an example and more details.', 'ncsu-a11y-helper' ) . '
', 75 | 'fields' => array( 76 | 'config_file' => array( 77 | 'title' => __( 'Configuration File Upload', 'ncsu-a11y-helper' ), 78 | 'type' => 'media', 79 | 'id' => 'config_file', 80 | ), 81 | ), 82 | ), 83 | 'info' => array( 84 | 'title' => __( 'About This Plugin', 'ncsu-a11y-helper' ), 85 | 'text' => '' . __( 'The NC State Accessibility Helper is a project of NC State University\'s Office of Information Technology; in particular, the OIT Design & Web Services team, the IT Accessibility Coordinator, and other helpful contributors. If you want to help improve this tool, please consider ', 'ncsu-a11y-helper' ) . '' . __( 'contributing on GitHub', 'ncsu-a11y-helper' ) . '!
' 86 | . '' . __( 'The majority of the accessibility tests included in this plugin are powered by ', 'ncsu-a11y-helper' ) . '' . __( 'aXe-core by Deque Systems', 'ncsu-a11y-helper' ) . '. ' . __( 'aXe-core is designed to avoid false positives, which provides a solid testing foundation. In some cases, we have written our own custom tests that sit on top of aXe, in order to prompt content creators to pay closer attention to common issues.', 'ncsu-a11y-helper' ) . '
' 87 | . '' . __( 'Everything we build online should be accessible, and the only way we can do that is if we work with all of our content creators and engage them in that effort. This plugin is about detecting and fixing issues before they\'re ever published, educating content creators about accessibility best practices, and (hopefully) inspiring our content creators to think about how they can help build a more inclusive user experience.', 'ncsu-a11y-helper' ) . '
', 88 | ), 89 | ), 90 | ), 91 | ); 92 | $option_page = new RationalOptionPages( $pages ); 93 | }); -------------------------------------------------------------------------------- /inc/publish_metabox.php: -------------------------------------------------------------------------------- 1 | id; 8 | 9 | if ( $ncsu_a11y_options['post_types'] ) { 10 | $checked_post_types = $ncsu_a11y_options['post_types']; 11 | } else { 12 | $checked_post_types = get_post_types( array( 'public' => true ) ); 13 | } 14 | 15 | if ( in_array($current_screen, $checked_post_types) ) { 16 | echo sprintf( 17 | '%s', 18 | get_preview_post_link( $post, array( 'ncsu_a11y' => 'true' ) ), 19 | 'Run Accessibility Check (opens in a new window)', 20 | 'Learn more about accessibility' 21 | ); 22 | 23 | } 24 | 25 | } 26 | add_action('post_submitbox_misc_actions', 'ncsu_a11y_run_button'); -------------------------------------------------------------------------------- /ncsu-a11y-helper.php: -------------------------------------------------------------------------------- 1 | ID; 39 | $post_type = get_post_type($post_id); 40 | $page_templates = wp_get_theme()->get_page_templates(); 41 | 42 | // Get meta data for our additional custom tests 43 | require_once( __NCSU_A11Y_HELPER_PATH__ . '/config/additional_tests_meta.php' ); 44 | $additional_tests_meta = json_encode($additional_tests_meta); 45 | 46 | // Get contents of custom config file as string 47 | if ( $ncsu_a11y_options['config_file'] ) { 48 | $config_file_path = get_attached_file( ncsu_a11y_get_attachment_id( $ncsu_a11y_options['config_file'] ) ); 49 | $config_str = json_decode( file_get_contents( $config_file_path, FILE_USE_INCLUDE_PATH ) ); 50 | $ncsu_defaults_json = null; 51 | } else { 52 | // If no custom config file, use ncsu_defaults.php 53 | require_once( __NCSU_A11Y_HELPER_PATH__ . '/config/ncsu_defaults.php' ); 54 | $config_str = null; 55 | $ncsu_defaults_json = json_encode($ncsu_defaults); 56 | } 57 | 58 | // Get wrapper class/id for this post type or page template 59 | if ( $post_type == 'page' && $page_templates ) { 60 | $my_template = ( get_page_template_slug($post_id) ) ? preg_replace('/\\.[^.\\s]{3,4}$/', '', get_page_template_slug($post_id) ) : 'page' ; 61 | 62 | $my_wrapper = ( $ncsu_a11y_options[str_replace( '-', '_', $my_template )] ) ? $ncsu_a11y_options[str_replace( '-', '_', $my_template )] : '.type-page'; 63 | 64 | } else { 65 | $my_wrapper = ( $ncsu_a11y_options[$post_type] ) ? $ncsu_a11y_options[$post_type] : '.' . $post_type; 66 | } 67 | 68 | $custom_options = array( 69 | 'wrapper' => $my_wrapper, 70 | 'additional_tests_meta' => $additional_tests_meta, 71 | 'ncsu_defaults' => $ncsu_defaults_json, 72 | 'config' => $config_str, 73 | ); 74 | 75 | if ( is_preview() && get_query_var('ncsu_a11y') == 'true' ) { 76 | // aXe: https://github.com/dequelabs/axe-core and https://www.deque.com/products/axe/ 77 | wp_register_script( 'axe-core', 'https://cdnjs.cloudflare.com/ajax/libs/axe-core/2.6.1/axe.min.js', array(), null, true ); 78 | wp_enqueue_script( 'axe-core' ); 79 | 80 | // Script to run aXe tests and generate annotated preview 81 | wp_register_script( 'a11y_tests', plugins_url('a11y_tests.js', __FILE__), array(), null, true ); 82 | wp_enqueue_script( 'a11y_tests' ); 83 | 84 | // Access plugin options in JS 85 | wp_localize_script( 'a11y_tests', 'custom_options', $custom_options ); 86 | 87 | // Script for annotated preview accessible modal 88 | wp_register_script( 'a11y-dialog', plugins_url('vendor/a11y-dialog/a11y-dialog.min.js', __FILE__), array(), null, true ); 89 | wp_enqueue_script( 'a11y-dialog' ); 90 | 91 | // Styles for annotated preview 92 | wp_register_style( 'a11y_styles', plugins_url('a11y_styles.css', __FILE__) ); 93 | wp_enqueue_style( 'a11y_styles' ); 94 | 95 | } 96 | 97 | } 98 | add_action( 'wp_enqueue_scripts', 'ncsu_a11y_helper__scripts_front' ); 99 | 100 | 101 | -------------------------------------------------------------------------------- /readme.txt: -------------------------------------------------------------------------------- 1 | === NC State Accessibility Helper === 2 | Contributors: OIT Design & Web Services, NC State University 3 | Donate link: https://design.oit.ncsu.edu/ 4 | Tags: accessibility 5 | Requires at least: 3.0.1 6 | Tested up to: 4.9.2 7 | License: GPLv2 or later 8 | License URI: http://www.gnu.org/licenses/gpl-2.0.html 9 | 10 | Checks for common accessibility issues users may have when creating content in the Visual Editor, and highlights them in an annotated preview 11 | 12 | == Description == 13 | 14 | Wouldn't it be great if content creators caught and fixed their accessibility issues *before* they hit the `Publish` button? 15 | 16 | The **NC State Accessibility Helper** tests your content and generates an annotated preview inside of WordPress. In the annotated preview, detected accessibility issues are highlighted, and the annotation describes the detected issue and links to a documentation resource where you can learn more about the issue and how to avoid it. 17 | 18 | [Contributions and collaboration are welcome!](https://github.com/briandeconinck/ncsu-a11y-helper) 19 | 20 | = How It Works = 21 | 22 | In the Accessibility Helper options (Dashboard > Settings > Accessibility Helper), select the post types that you wish to be checkable using the Accessibility Helper. By default, the Accessibility Helper will check the contents of the `.type-[post-type]` element, but you can change this on the options page as well. 23 | 24 | When editing a post, page, or custom post type for which the Accessibility Helper is enabled, you will see a "Run Accessibility Check" button in the "Publish" metabox, just above the Publish/Update button. 25 | 26 | Clicking the "Run Accessibility Check" button will open an annotated preview. This is similar to a normal post preview, but with two additional JavaScript files and one additional CSS file. These JavaScript files perform a variety of tests on the contents of that preview. For most tests, we use the [aXe Accessibility Engine](https://github.com/dequelabs/axe-core) by [Deque Systems](https://www.deque.com/) to perform accessibility tests on content in WordPress. 27 | 28 | The plugin also defines and tests some custom rules. aXe is very conservative and tries to avoid all false positives, and so doesn't test anything they can't be certain about. But there are some cases where we'd rather have a false positive that puts helpful information in front of our users and gets them thinking about best practices. For those situations, we write a custom rule. 29 | 30 | If the JS file detects an issue, it will: 31 | * Wrap the offending code in a `span`, which we style to indicate the impact of the issue 32 | * Append an annotation to the offending code 33 | * Generates a modal dialog that users can view to see more information about the detected issue 34 | 35 | aXe categorizes detected issues with impact levels: 36 | * Critical 37 | * Serious 38 | * Moderate 39 | * Minor 40 | 41 | For our custom rules, we either assign one of those impact levels, or otherwise assign an impact of **Info**. 42 | 43 | For aXe tests, annotations use the default help text and help URL provided by aXe. In cases where we'd rather write our own or send users to a different help resource, we can override that in either the `ncsu_defaults.php` file or by uploading a custom configuration file on a site-by-site basis. 44 | 45 | We have replaced all of the documentation links provided by aXe with NC State Go Links, so that we can track clicks and get a sense for the kinds of problems our users are encountering. These Go Links currently redirect to Deque's documentation, but may eventually point to alternative documentation for issues that we want to more carefully discuss for users. 46 | 47 | == Installation == 48 | 49 | This plugin is available to NC State through Cthulhu. (Not sure what that means? Contact the Help Desk for more information.) 50 | 51 | Off-campus users can install the plugin via a [public GitHub repository](https://github.com/briandeconinck/ncsu-a11y-helper) using Andy Fragen's [GitHub Updater plugin](https://github.com/afragen/github-updater). 52 | 53 | == Changelog == 54 | 55 | = 1.0.0 = 56 | * Major revisions, now stable enough for a 1.0 tag 57 | * Add additional custom tests 58 | * Make helper text more generic, less NC State-y 59 | * Resolves some accessibility issues in the plugin itself 60 | 61 | = 0.2.0 = 62 | * Major revisions 63 | * Change annotated preview to take advantage of WordPress's native Preview rather than using a metabox 64 | * Added options page for customizing which post types are checked, which elements within those post types contain the content, and customizing which tests are run and what messages are displayed to users 65 | * Complete rewrite of most of PHP and non-aXe JavaScript files 66 | 67 | = 0.1.0 = 68 | * Initial release to campus 69 | 70 | == Upgrade Notice == 71 | 72 | = 1.0.0 = 73 | General improvements, plugin now more stable 74 | 75 | = 0.2.0 = 76 | Almost complete rewrite of the plugin 77 | 78 | = 0.1.0 = 79 | Initial release to campus 80 | -------------------------------------------------------------------------------- /vendor/RationalOptionPages/RationalOptionPages.php: -------------------------------------------------------------------------------- 1 | 8 | * @copyright Copyright (c) 2016 9 | * @link http://jeremyhixon.com 10 | * @version 1.0.0 11 | */ 12 | 13 | class RationalOptionPages { 14 | /* ========================================================================== 15 | Vars 16 | ========================================================================== */ 17 | protected $attributes = array( 18 | 'input' => array( 19 | 'autocomplete' => false, 20 | 'autofocus' => false, 21 | 'disabled' => false, 22 | 'list' => false, 23 | 'max' => false, 24 | 'maxlength' => false, 25 | 'min' => false, 26 | 'pattern' => false, 27 | 'readonly' => false, 28 | 'required' => false, 29 | 'size' => false, 30 | 'step' => false, 31 | ), 32 | 'select' => array( 33 | 'multiple' => false, 34 | 'size' => 4, 35 | ), 36 | 'textarea' => array( 37 | 'cols' => 20, 38 | 'rows' => 2, 39 | 'wrap' => 'soft', 40 | ), 41 | ); 42 | protected $defaults = array( 43 | 'add_menu_page' => array( 44 | 'page_title' => 'Option Page', 45 | 'menu_title' => 'Option Page', 46 | 'capability' => 'manage_options', 47 | 'menu_slug' => 'option_page', 48 | 'callback' => false, 49 | 'icon_url' => false, 50 | 'position' => null, 51 | ), 52 | 'add_settings_field' => array( 53 | 'id' => 'settings_field', 54 | 'title' => 'Settings Field', 55 | 'callback' => false, 56 | 'page' => 'option_page', 57 | 'section' => 'settings_section', 58 | 'args' => false, 59 | ), 60 | 'add_settings_section' => array( 61 | 'id' => 'settings_section', 62 | 'title' => 'Settings Section', 63 | 'callback' => false, 64 | 'page' => 'option_page', 65 | ), 66 | 'add_submenu_page' => array( 67 | 'parent_slug' => 'option_page', 68 | 'page_title' => 'Sub Option Page', 69 | 'menu_title' => 'Sub Option Page', 70 | 'capability' => 'manage_options', 71 | 'menu_slug' => 'sub_option_page', 72 | 'callback' => false, 73 | ), 74 | ); 75 | protected $errors; 76 | protected $fields = array( 77 | 'checkbox' => array( 78 | 'checked' => false, 79 | 'value' => 'on', 80 | ), 81 | 'text' => array( 82 | 'class' => 'regular-text', 83 | 'placeholder' => '', 84 | 'value' => false, 85 | ), 86 | 'textarea' => array( 87 | 'class' => 'large-text', 88 | 'placeholder' => '', 89 | 'rows' => 10, 90 | 'value' => false, 91 | ), 92 | 'wp_editor' => array( 93 | 'wpautop' => true, 94 | 'media_buttons' => true, 95 | 'textarea_rows' => 'default', 96 | 'tabindex' => false, 97 | 'editor_css' => false, 98 | 'editor_class' => '', 99 | 'editor_height' => false, 100 | 'teeny' => false, 101 | 'dfw' => false, 102 | 'tinymce' => true, 103 | 'quicktags' => true, 104 | 'drag_drop_upload' => false, 105 | ), 106 | ); 107 | protected $media_script = false; 108 | protected $notices; 109 | protected $options; 110 | protected $pages = array(); 111 | protected $subpages = array(); 112 | protected $points; 113 | 114 | /* ========================================================================== 115 | Magic methods 116 | ========================================================================== */ 117 | /** 118 | * Catches unknown method calls 119 | * 120 | * @param string $method The method being requested 121 | * @param array $arguments Array of arguments passed to the method 122 | */ 123 | public function __call( $method, $arguments ) { 124 | $request = explode( '|', $method ); 125 | $source = $request[0]; 126 | $page_key = !empty( $request[1] ) ? $request[1] : false; 127 | $section_key = !empty( $request[2] ) ? $request[2] : false; 128 | $field_key = !empty( $request[3] ) ? $request[3] : false; 129 | 130 | switch ( $source ) { 131 | case 'add_menu_page': 132 | case 'add_submenu_page': 133 | $this->build_menu_page( $page_key ); 134 | break; 135 | case 'add_settings_section': 136 | $this->build_settings_section( $page_key, $section_key ); 137 | break; 138 | case 'add_settings_field': 139 | $this->build_settings_field( $page_key, $section_key, $field_key ); 140 | break; 141 | case 'register_setting': 142 | $input = $this->sanitize_setting( $page_key, $arguments[0] ); 143 | return $input; 144 | break; 145 | default: 146 | $this->submit_notice( $method ); 147 | } 148 | } 149 | 150 | /** 151 | * Class construct method. Configures class and hooks into WordPress. 152 | * 153 | * @param array $pages Array of option pages 154 | */ 155 | public function __construct( $pages = array() ) { 156 | foreach ( $pages as $page_key => $page_params ) { 157 | $this->pages[ $page_key ] = $this->validate_page( $page_key, $page_params ); 158 | } 159 | $this->pages = array_merge( $this->pages, $this->subpages ); 160 | 161 | add_action( 'admin_enqueue_scripts', array( $this, 'admin_enqueue_scripts' ) ); 162 | add_action( 'admin_head', array( $this, 'admin_head' ) ); 163 | add_action( 'admin_init', array( $this, 'admin_init' ) ); 164 | add_action( 'admin_menu', array( $this, 'admin_menu' ) ); 165 | add_action( 'admin_notices', array( $this, 'admin_notices' ) ); 166 | } 167 | 168 | /* ========================================================================== 169 | WordPress hooks 170 | ========================================================================== */ 171 | /** 172 | * Action: admin_enqueue_scripts 173 | * Conditionally queue's up jQuery and the media uploader script 174 | */ 175 | public function admin_enqueue_scripts() { 176 | if ( $this->media_script ) { 177 | wp_enqueue_script( 'jquery' ); 178 | wp_enqueue_media(); 179 | } 180 | } 181 | 182 | /** 183 | * Action: admin_head 184 | * Conditionally adds the script to manage media uploads 185 | */ 186 | public function admin_head() { 187 | if ( $this->media_script ) { 188 | ?> 226 | pages as $page_key => $page_params ) { 235 | // Finalize sanitize 236 | if ( empty( $page_params['custom'] ) && !is_array( $page_params['sanitize'] ) ) { 237 | $page_params['sanitize'] = array( $this, $page_params['sanitize'] ); 238 | } 239 | 240 | register_setting( 241 | $page_key, 242 | $page_key, 243 | $page_params['sanitize'] 244 | ); 245 | 246 | if ( !empty( $page_params['sections'] ) ) { 247 | foreach ( $page_params['sections'] as $section_key => $section_params ) { 248 | // Sort and trim the array for the function 249 | $sort_order = array_keys( $this->defaults['add_settings_section'] ); 250 | $params = $this->sort_array( $section_params, $sort_order ); 251 | $params = array_slice( $params, 0, count( $this->defaults['add_settings_section'] ) ); 252 | 253 | // Finalize callback 254 | if ( empty( $params['custom'] ) && !is_array( $params['callback'] ) ) { 255 | $params['callback'] = array( $this, $params['callback'] ); 256 | } 257 | 258 | call_user_func_array( 'add_settings_section', $params ); 259 | 260 | if ( !empty( $section_params['fields'] ) ) { 261 | foreach ( $section_params['fields'] as $field_key => $field_params ) { 262 | // Check for "media" type for adding script 263 | if ( !$this->media_script && $field_params['type'] === 'media' ) { 264 | $this->media_script = true; 265 | } 266 | 267 | // Sort and trim the array for the function 268 | $sort_order = array_keys( $this->defaults['add_settings_field'] ); 269 | $params = $this->sort_array( $field_params, $sort_order ); 270 | $params = array_slice( $params, 0, count( $this->defaults['add_settings_field'] ) ); 271 | 272 | // Add label wrapper on title 273 | if ( 274 | !in_array( $field_params['type'], array( 'radio' ) ) && 275 | ( empty( $field_params['no_label'] ) || $field_params['no_label'] === false ) 276 | ) { 277 | $params['title'] = ""; 278 | } 279 | 280 | // Finalize callback 281 | if ( empty( $params['custom'] ) && !is_array( $params['callback'] ) ) { 282 | $params['callback'] = array( $this, $params['callback'] ); 283 | } 284 | 285 | call_user_func_array( 'add_settings_field', $params ); 286 | } 287 | } 288 | } 289 | } 290 | } 291 | } 292 | 293 | /** 294 | * Action: admin_menu. Adding the option pages to the admin menu. 295 | */ 296 | public function admin_menu() { 297 | $all_pages = array_merge( $this->pages, $this->subpages ); 298 | 299 | foreach ( $all_pages as $page ) { 300 | // Sort and trim the array for the function 301 | $sort_order = array_keys( $this->defaults[ $page['function'] ] ); 302 | $params = $this->sort_array( $page, $sort_order ); 303 | $params = array_slice( $params, 0, count( $this->defaults[ $page['function'] ] ) ); 304 | 305 | // Finalize callback 306 | $params['callback'] = array( $this, $params['callback'] ); 307 | 308 | call_user_func_array( $page['function'], $params ); 309 | } 310 | } 311 | 312 | /** 313 | * Action: admin_notices. Spitting out notices when needed. 314 | */ 315 | public function admin_notices() { 316 | // notice-error, notice-warning, notice-success, or notice-info. 317 | if ( !empty( $this->errors ) ) { 318 | foreach ( $this->errors as $error ) { 319 | echo $error; 320 | } 321 | } 322 | if ( !empty( $this->notices ) ) { 323 | foreach ( $this->notices as $notice ) { 324 | echo $notice; 325 | } 326 | } 327 | 328 | // update point in array for future reference 329 | $this->points['admin_notices'] = true; 330 | } 331 | 332 | /* ========================================================================== 333 | Helpers 334 | ========================================================================== */ 335 | public function add_page( $page_key, $page_params ) { 336 | $this->pages[ $page_key ] = $this->validate_page( $page_key, $page_params ); 337 | } 338 | 339 | /** 340 | * Builds the menu page 341 | * 342 | * @param string $page_key The array key of the page needing built 343 | */ 344 | protected function build_menu_page( $page_key ) { 345 | $page = $this->pages[ $page_key ]; 346 | $this->options = get_option( $page_key, array() ); 347 | ?>{$field['text']}
" : '' // text 427 | ); 428 | break; 429 | case 'checkbox': 430 | echo ''; 459 | break; 460 | case 'radio': 461 | echo ''; 482 | break; 483 | case 'select': 484 | if (!empty($field['attributes']) && isset($field['attributes']['multiple']) && $field['attributes']['multiple']) { 485 | $field_tag_name = "{$page_key}[{$field['id']}][]"; 486 | $field_name = "{$field['id']}[]"; 487 | } 488 | else { 489 | $field_tag_name = "{$page_key}[{$field['id']}]"; 490 | $field_name = "{$field['id']}"; 491 | } 492 | printf( 493 | ''; 524 | break; 525 | case 'textarea': 526 | printf( 527 | '%s', 528 | !empty( $field['class'] ) ? "class='{$field['class']}'" : '', // class 529 | $field['id'], // id 530 | "{$page_key}[{$field['id']}]", // name 531 | !empty( $field['placeholder'] ) ? "placeholder='{$field['placeholder']}'" : '', // placeholder 532 | !empty( $field['rows'] ) ? "rows='{$field['rows']}'" : '', // rows 533 | $field['title_attr'], // title 534 | $field['value'], // value 535 | !empty( $field['text'] ) ? "{$field['text']}
" : '' // text 536 | ); 537 | break; 538 | case 'wp_editor': 539 | $field['textarea_name'] = "{$page_key}[{$field['id']}]"; 540 | wp_editor( $field['value'], $field['id'], array( 541 | 'textarea_name' => $field['textarea_name'], 542 | ) ); 543 | echo !empty( $field['text'] ) ? "{$field['text']}
" : ''; 544 | break; 545 | default: 546 | printf( 547 | '%s', 548 | !empty( $field['class'] ) ? "class='{$field['class']}'" : '', // class 549 | $field['id'], // id 550 | "{$page_key}[{$field['id']}]", // name 551 | !empty( $field['placeholder'] ) ? "placeholder='{$field['placeholder']}'" : '', // placeholder 552 | $field['title_attr'], // title 553 | $field['type'], // type 554 | $field['value'], // value 555 | !empty( $attributes ) ? implode( ' ', $attributes ) : '', // additional attributes 556 | !empty( $field['text'] ) ? "{$field['text']}
" : '' // text 557 | ); 558 | } 559 | } 560 | 561 | /** 562 | * Builds the settings sections 563 | * 564 | * @param string $page_key The array key of the page 565 | * @param type $section_key The array key of the section 566 | */ 567 | protected function build_settings_section( $page_key, $section_key ) { 568 | $page = $this->pages[ $page_key ]; 569 | $section = $page['sections'][ $section_key ]; 570 | 571 | echo !empty( $section['text'] ) ? $section['text'] : ''; 572 | 573 | if ( !empty( $section['include'] ) ) { 574 | include $section['include']; 575 | } 576 | } 577 | 578 | /** 579 | * Determines if the option page has fields or not 580 | * 581 | * @param array $page The page array 582 | * 583 | * @return boolean True if fields are found, false otherwise 584 | */ 585 | protected function has_fields( $page ) { 586 | if ( !empty( $page['sections'] ) ) { 587 | foreach ( $page['sections'] as $section ) { 588 | if ( !empty( $section['fields'] ) ) { 589 | return true; 590 | } 591 | } 592 | } 593 | return false; 594 | } 595 | 596 | /** 597 | * Cleans up the option page submissions before submitting to the DB 598 | * 599 | * @param string $page_key The array key of the page 600 | * 601 | * @return array The sanitized post input 602 | */ 603 | protected function sanitize_setting( $page_key, $input ) { 604 | $page = $this->pages[ $page_key ]; 605 | 606 | if ( !empty( $page['sections'] ) ) { 607 | foreach ( $page['sections'] as $section ) { 608 | if ( !empty( $section['fields'] ) ) { 609 | foreach ( $section['fields'] as $field ) { 610 | switch ( $field['type'] ) { 611 | case 'checkbox': 612 | if ( empty( $input[ $field['id'] ] ) ) { 613 | $input[ $field['id'] ] = false; 614 | } 615 | break; 616 | default: 617 | // Sanitize by default; skip if this field's 'sanitize' setting is false. 618 | if ( !isset($field['sanitize']) || $field['sanitize'] ) { 619 | $input[ $field['id'] ] = strip_tags($input[ $field['id'] ]); 620 | $input[ $field['id'] ] = esc_attr($input[ $field['id'] ]); 621 | } 622 | } 623 | } 624 | } 625 | } 626 | } 627 | 628 | return $input; 629 | } 630 | 631 | /** 632 | * Converts human-readable strings into more machine-friendly formats 633 | * 634 | * @param string $text String to be formatted 635 | * @param string $separator The character that fills in spaces 636 | * 637 | * @return string Formatted text 638 | */ 639 | protected function slugify( $text, $separator = '_' ) { 640 | $text = preg_replace( '~[^\\pL\d]+~u', $separator, $text ); 641 | $text = trim( $text, $separator ); 642 | $text = iconv( 'utf-8', 'us-ascii//TRANSLIT', $text ); 643 | $text = strtolower( $text ); 644 | $text = preg_replace( '~[^-\w]+~', '', $text ); 645 | if ( empty( $text ) ) { 646 | return 'n-a'; 647 | } 648 | return $text; 649 | } 650 | 651 | /** 652 | * Sorts one array using a second as a guide 653 | * 654 | * @param array $array Array to be sorted 655 | * @param array $order_array Guide array 656 | * 657 | * @return array Sorted array 658 | */ 659 | protected function sort_array( $array, $order_array ) { 660 | $ordered = array(); 661 | foreach ( $order_array as $key ) { 662 | if ( array_key_exists( $key, $array ) ) { 663 | $ordered[ $key ] = $array[ $key ]; 664 | unset( $array[ $key ] ); 665 | } 666 | } 667 | return $ordered + $array; 668 | } 669 | 670 | /** 671 | * Conditionally outputs an error in WordPress admin 672 | * 673 | * @param string $error The error to be output 674 | */ 675 | public function submit_error( $error ) { 676 | $error = sprintf( 677 | '%s
' . htmlspecialchars( print_r( $error, true ) ) . '' : $error 679 | ); 680 | if ( empty( $this->points['admin_notices'] ) ) { 681 | $this->errors[] = $error; 682 | } else { 683 | echo $error; 684 | } 685 | } 686 | 687 | /** 688 | * Conditionally outputs a notice in WordPress admin 689 | * 690 | * @param string $notice The text to be output 691 | */ 692 | public function submit_notice( $notice ) { 693 | $notice = sprintf( 694 | '
%s
' . htmlspecialchars( print_r( $notice, true ) ) . '' : $notice 696 | ); 697 | if ( empty( $this->points['admin_notices'] ) ) { 698 | $this->notices[] = $notice; 699 | } else { 700 | echo $notice; 701 | } 702 | } 703 | 704 | /** 705 | * Validates the field data submitted to the class 706 | * 707 | * @param array $field Field array 708 | * @param string $page_key Array key of the associated page 709 | * @param string $section_key Array key of the associated section 710 | * @param string $field_key Array key of the field 711 | * @param string $page ID of the associated page 712 | * @param type $section ID of the associated section 713 | * 714 | * @return array The validated field array 715 | */ 716 | protected function validate_field( $field, $page_key, $section_key, $field_key, $page, $section ) { 717 | // Label 718 | if ( empty( $field['title'] ) ) { 719 | $this->submit_error( 'Field parameter "title" is required' ); 720 | } 721 | 722 | // ID 723 | if ( empty( $field['id'] ) ) { 724 | $field['id'] = $this->slugify( $field['title'] ); 725 | } 726 | 727 | // Callback 728 | $field['callback'] = empty( $field['callback'] ) ? "add_settings_field|{$page_key}|{$section_key}|{$field_key}" : $field['callback']; 729 | 730 | // Page 731 | $field['page'] = $page; 732 | 733 | // Section 734 | $field['section'] = $section; 735 | 736 | // Type 737 | $field['type'] = empty( $field['type'] ) ? 'text' : $field['type']; 738 | 739 | // Title attribute 740 | $field['title_attr'] = empty( $field['title_attr'] ) ? $field['title'] : $field['title_attr']; 741 | 742 | // Choices 743 | if ( empty( $field['choices'] ) && in_array( $field['type'], array( 'radio', 'select' ) ) ) { 744 | $this->submit_error( 'Field parameter "choices" is required for the "radio" and "select" type' ); 745 | } 746 | 747 | // Other attributes 748 | if ( !empty( $field['attributes'] ) ) { 749 | switch ( $field['type'] ) { 750 | case 'select': 751 | case 'textarea': 752 | $field['attributes'] = wp_parse_args( $field['attributes'], $this->attributes[ $field['type'] ] ); 753 | break; 754 | default: 755 | $field['attributes'] = wp_parse_args( $field['attributes'], $this->attributes['input'] ); 756 | } 757 | } 758 | 759 | // Making sure we haven't missed anything 760 | switch ( $field['type'] ) { 761 | case 'checkbox': 762 | $field = wp_parse_args( $field, $this->fields['checkbox'] ); 763 | break; 764 | case 'color': 765 | case 'radio': 766 | case 'range': 767 | break; 768 | case 'date': 769 | $field['value'] = date( 'Y-m-d', strtotime( $field['value'] ) ); 770 | $field = wp_parse_args( $field, $this->fields['text'] ); 771 | break; 772 | case 'datetime': 773 | case 'datetime-local': 774 | $field['value'] = date( 'Y-m-d\TH:i:s', strtotime( $field['value'] ) ); 775 | $field = wp_parse_args( $field, $this->fields['text'] ); 776 | break; 777 | case 'month': 778 | $field['value'] = date( 'Y-m', strtotime( $field['value'] ) ); 779 | $field = wp_parse_args( $field, $this->fields['text'] ); 780 | break; 781 | case 'textarea': 782 | $field = wp_parse_args( $field, $this->fields[ $field['type'] ] ); 783 | break; 784 | case 'time': 785 | $field['value'] = date( 'H:i:s', strtotime( $field['value'] ) ); 786 | $field = wp_parse_args( $field, $this->fields['text'] ); 787 | break; 788 | case 'week': 789 | $field['value'] = date( 'Y-\WW', strtotime( $field['value'] ) ); 790 | $field = wp_parse_args( $field, $this->fields['text'] ); 791 | break; 792 | case 'wp_editor': 793 | $field = wp_parse_args( $field, $this->fields['wp_editor'] ); 794 | break; 795 | default: 796 | $field = wp_parse_args( $field, $this->fields['text'] ); 797 | } 798 | 799 | return $field; 800 | } 801 | 802 | /** 803 | * Validates the information submitted to the class 804 | * 805 | * @param string $page_key Array key of the page 806 | * @param array $page Array of page parameters 807 | * @param string $parent_slug Menu slug of the parent page if there is one 808 | * 809 | * @return array Validated array of page parameters 810 | */ 811 | protected function validate_page( $page_key, $page_params, $parent_slug = false ) { 812 | // Page title 813 | if ( empty( $page_params['page_title'] ) ) { 814 | $this->submit_error( 'Page parameter "page_title" is required' ); 815 | } 816 | 817 | // Menu title 818 | if ( empty( $page_params['menu_title'] ) ) { 819 | $page_params['menu_title'] = $page_params['page_title']; 820 | } 821 | 822 | // Menu slug 823 | if ( empty( $page_params['menu_slug'] ) ) { 824 | // Basing it off the page title cause it's likely to be more unique than the menu title 825 | $page_params['menu_slug'] = $this->slugify( $page_params['page_title'] ); 826 | } 827 | 828 | // Menu or submenu item? 829 | if ( empty( $page_params['parent_slug'] ) && !$parent_slug ) { 830 | $page_params['function'] = 'add_menu_page'; 831 | } else { 832 | $page_params['function'] = 'add_submenu_page'; 833 | $page_params['parent_slug'] = $parent_slug ? $parent_slug : $page_params['parent_slug']; 834 | } 835 | 836 | // Callback 837 | $page_params['callback'] = "{$page_params['function']}|{$page_key}"; 838 | 839 | // Sanitize 840 | $page_params['sanitize'] = empty( $page_params['sanitize'] ) ? "register_setting|{$page_key}" : $page_params['sanitize']; 841 | 842 | // Make sure we haven't missed anything 843 | $page_params = wp_parse_args( $page_params, $this->defaults[ $page_params['function'] ] ); 844 | 845 | // Subpages? 846 | if ( !empty( $page_params['subpages'] ) ) { 847 | foreach ( $page_params['subpages'] as $subpage_key => $subpage ) { 848 | $this->subpages[ $subpage_key ] = $this->validate_page( $subpage_key, $subpage, $page_params['menu_slug'] ); 849 | } 850 | unset( $page_params['subpages'] ); 851 | } 852 | 853 | // Sections? 854 | if ( !empty( $page_params['sections'] ) ) { 855 | foreach ( $page_params['sections'] as $section_key => $section_params ) { 856 | $page_params['sections'][ $section_key ] = $this->validate_section( $section_params, $page_key, $section_key, $page_params['menu_slug'] ); 857 | } 858 | } 859 | 860 | return $page_params; 861 | } 862 | 863 | /** 864 | * Validates the section data submitted to the class 865 | * 866 | * @param array $section Section array 867 | * @param string $page_key Array key of the associated page 868 | * @param string $section_key Array key of the associated page 869 | * @param string $page ID of the associated page 870 | * 871 | * @return array Validated section array 872 | */ 873 | protected function validate_section( $section, $page_key, $section_key, $page ) { 874 | // Title 875 | if ( empty( $section['title'] ) ) { 876 | $this->submit_error( 'Section parameter "title" is required' ); 877 | } 878 | 879 | // ID 880 | if ( empty( $section['id'] ) ) { 881 | $section['id'] = $this->slugify( $section['title'] ); 882 | } 883 | 884 | // Callback 885 | $section['callback'] = empty( $section['callback'] ) ? "add_settings_section|{$page_key}|{$section_key}" : $section['callback']; 886 | 887 | // Page 888 | $section['page'] = $page; 889 | 890 | // Fields? 891 | if ( !empty( $section['fields'] ) ) { 892 | foreach ( $section['fields'] as $field_key => $field_params ) { 893 | $section['fields'][ $field_key ] = $this->validate_field( $field_params, $page_key, $section_key, $field_key, $page, $section['id'] ); 894 | } 895 | } 896 | 897 | return $section; 898 | } 899 | } -------------------------------------------------------------------------------- /vendor/a11y-dialog/LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | Copyright (c) 2017 Edenspiekermann 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 5 | 6 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 7 | 8 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 9 | -------------------------------------------------------------------------------- /vendor/a11y-dialog/a11y-dialog.min.js: -------------------------------------------------------------------------------- 1 | /*! a11y-dialog 3.0.2 — © Edenspiekermann */ 2 | !function(t){"use strict";function e(t,e){this._show=this.show.bind(this),this._hide=this.hide.bind(this),this._maintainFocus=this._maintainFocus.bind(this),this._bindKeypress=this._bindKeypress.bind(this),this.node=t,this._listeners={},this.create(e)}function i(t){return Array.prototype.slice.call(t)}function n(t,e){return i((e||document).querySelectorAll(t))}function s(t){return NodeList.prototype.isPrototypeOf(t)?i(t):Element.prototype.isPrototypeOf(t)?[t]:"string"==typeof t?n(t):void 0}function o(t){var e=r(t);e.length&&e[0].focus()}function r(t){return n(c.join(","),t).filter(function(t){return!!(t.offsetWidth||t.offsetHeight||t.getClientRects().length)})}function h(t,e){var i=r(t),n=i.indexOf(document.activeElement);e.shiftKey&&0===n?(i[i.length-1].focus(),e.preventDefault()):e.shiftKey||n!==i.length-1||(i[0].focus(),e.preventDefault())}function d(t){var e=i(t.parentNode.childNodes),n=e.filter(function(t){return 1===t.nodeType});return n.splice(n.indexOf(t),1),n}var a,c=["a[href]","area[href]","input:not([disabled])","select:not([disabled])","textarea:not([disabled])","button:not([disabled])","iframe","object","embed","[contenteditable]",'[tabindex]:not([tabindex^="-"])'];e.prototype.create=function(t){return this._targets=this._targets||s(t)||d(this.node),this.node.setAttribute("aria-hidden",!0),this.shown=!1,this._openers=n('[data-a11y-dialog-show="'+this.node.id+'"]'),this._openers.forEach(function(t){t.addEventListener("click",this._show)}.bind(this)),this._closers=n("[data-a11y-dialog-hide]",this.node).concat(n('[data-a11y-dialog-hide="'+this.node.id+'"]')),this._closers.forEach(function(t){t.addEventListener("click",this._hide)}.bind(this)),this._fire("create"),this},e.prototype.show=function(t){return this.shown?this:(this.shown=!0,this.node.removeAttribute("aria-hidden"),this._targets.forEach(function(t){var e=t.getAttribute("aria-hidden");e&&t.setAttribute("data-a11y-dialog-original",e),t.setAttribute("aria-hidden","true")}),a=document.activeElement,o(this.node),document.body.addEventListener("focus",this._maintainFocus,!0),document.addEventListener("keydown",this._bindKeypress),this._fire("show",t),this)},e.prototype.hide=function(t){return this.shown?(this.shown=!1,this.node.setAttribute("aria-hidden","true"),this._targets.forEach(function(t){var e=t.getAttribute("data-a11y-dialog-original");e?(t.setAttribute("aria-hidden",e),t.removeAttribute("data-a11y-dialog-original")):t.removeAttribute("aria-hidden")}),a&&a.focus(),document.body.removeEventListener("focus",this._maintainFocus,!0),document.removeEventListener("keydown",this._bindKeypress),this._fire("hide",t),this):this},e.prototype.destroy=function(){return this.hide(),this._openers.forEach(function(t){t.removeEventListener("click",this._show)}.bind(this)),this._closers.forEach(function(t){t.removeEventListener("click",this._hide)}.bind(this)),this._fire("destroy"),this._listeners={},this},e.prototype.on=function(t,e){return void 0===this._listeners[t]&&(this._listeners[t]=[]),this._listeners[t].push(e),this},e.prototype.off=function(t,e){var i=this._listeners[t].indexOf(e);return i>-1&&this._listeners[t].splice(i,1),this},e.prototype._fire=function(t,e){var i=this._listeners[t]||[],n=e?e.target:void 0;i.forEach(function(t){t(this.node,n)}.bind(this))},e.prototype._bindKeypress=function(t){this.shown&&27===t.which&&(t.preventDefault(),this.hide()),this.shown&&9===t.which&&h(this.node,t)},e.prototype._maintainFocus=function(t){this.shown&&!this.node.contains(t.target)&&o(this.node)},"undefined"!=typeof module&&void 0!==module.exports?module.exports=e:"function"==typeof define&&define.amd?define("A11yDialog",[],function(){return e}):"object"==typeof t&&(t.A11yDialog=e)}("undefined"!=typeof global?global:window); 3 | --------------------------------------------------------------------------------