├── icon.png ├── inc ├── misc.php ├── publish_metabox.php ├── info_metabox.php └── options.php ├── vendor ├── a11y-dialog │ ├── LICENSE │ └── a11y-dialog.min.js └── RationalOptionPages │ └── RationalOptionPages.php ├── config ├── ncsu_defaults.php └── additional_tests_meta.php ├── ncsu-a11y-helper.php ├── readme.txt ├── a11y_styles.css └── a11y_tests.js /icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OIT-Design/ncsu-a11y-helper/HEAD/icon.png -------------------------------------------------------------------------------- /inc/misc.php: -------------------------------------------------------------------------------- 1 | get_col($wpdb->prepare("SELECT ID FROM $wpdb->posts WHERE guid='%s';", $attachment_url )); 14 | return $attachment[0]; 15 | } -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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'); -------------------------------------------------------------------------------- /config/ncsu_defaults.php: -------------------------------------------------------------------------------- 1 | 'http://go.ncsu.edu/a11y_' . $test . ':' . $site_url 83 | ); 84 | } 85 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /inc/info_metabox.php: -------------------------------------------------------------------------------- 1 | true ) ); 13 | } 14 | 15 | add_meta_box( 'ncsu_a11y', 'Accessibility Helper', 'ncsu_a11y_meta_content', $checked_post_types, 'normal', 'default', null ); 16 | } 17 | add_action( 'add_meta_boxes', 'ncsu_a11y_meta_box' ); 18 | 19 | function ncsu_a11y_meta_content() { 20 | 21 | $default_meta_text = '' . __( '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 | }); -------------------------------------------------------------------------------- /config/additional_tests_meta.php: -------------------------------------------------------------------------------- 1 | array( 5 | "id" => "ncsu_skipped_heading", 6 | "impact" => "serious", 7 | "help" => "Do not skip heading levels. Headings should not increment by more than 1.", 8 | "helpUrl" => "https://accessibility.oit.ncsu.edu/it-accessibility-at-nc-state/developers/accessibility-handbook/headings/", 9 | "nodes" => array( 10 | 0 => array( 11 | "failureSummary" => "Change your heading so that it is at most 1 larger than the previous heading (eg. if the previous heading was a Heading 2, this heading could be a Heading 3, but not a Heading 4).", 12 | "target" => array( 13 | 0 => null, 14 | ) 15 | ) 16 | ), 17 | "description" => "Headings should be used to denote sections and subsections of content, and provide structure to your content, especially for those using screen-reading software. Skipping heading levels can be disorienting.", 18 | "html" => null, 19 | ), 20 | "ncsu_multiple_h1" => array( 21 | "id" => "ncsu_multiple_h1", 22 | "impact" => "info", 23 | "help" => "In most cases, you should not use Heading 1's in your content.", 24 | "helpUrl" => "https://accessibility.oit.ncsu.edu/it-accessibility-at-nc-state/developers/accessibility-handbook/headings/", 25 | "nodes" => array( 26 | 0 => array( 27 | "failureSummary" => "Change your heading to a different heading level. Headings should be used to provide structure (like an outline), not to emphasize important content.", 28 | "target" => array( 29 | 0 => null, 30 | ) 31 | ) 32 | ), 33 | "description" => "In most cases, your page or post title will be a Heading 1. It should be the only Heading 1 in your post or page.", 34 | "html" => null, 35 | ), 36 | "ncsu_empty_alt" => array( 37 | "id" => "ncsu_empty_alt", 38 | "impact" => "moderate", 39 | "help" => "Image alt attributes should be empty only if the image is purely decorative.", 40 | "helpUrl" => "https://accessibility.oit.ncsu.edu/it-accessibility-at-nc-state/developers/accessibility-handbook/alternative-text/", 41 | "nodes" => array( 42 | 0 => array( 43 | "failureSummary" => "If your image is not purely decorative, add a text description of the contents of the image in the image's alt attribute.", 44 | "target" => array( 45 | 0 => null, 46 | ) 47 | ) 48 | ), 49 | "description" => "If your image is purely decorative (no information is being communicated to the user by including the image), then empty alt text is acceptable. Otherwise, you must provide the text equivalent of the image in the alt attribute.", 50 | "html" => null, 51 | ), 52 | "ncsu_reminder_alt" => array( 53 | "id" => "ncsu_reminder_alt", 54 | "impact" => "info", 55 | "help" => "Double-check your alt text to be sure it describes the contents of the image.", 56 | "helpUrl" => "https://accessibility.oit.ncsu.edu/it-accessibility-at-nc-state/developers/accessibility-handbook/alternative-text/", 57 | "nodes" => array( 58 | 0 => array( 59 | "failureSummary" => "Double-check your alt text. Make sure it's not just a filename or non-descriptive placeholder text.", 60 | "target" => array( 61 | 0 => null, 62 | ) 63 | ) 64 | ), 65 | "description" => "Alt attributes should provide a text equivalent of any information being communicated by the image. This is often a description of the contents of the image, emphasizing any key details or points users are expected to recognize.", 66 | "html" => null, 67 | ), 68 | "ncsu_reminder_table" => array( 69 | "id" => "ncsu_reminder_table", 70 | "impact" => "info", 71 | "help" => "Be sure to use tables for tabular information only, eg. data tables. Do not use tables for layout.", 72 | "helpUrl" => "https://accessibility.oit.ncsu.edu/it-accessibility-at-nc-state/developers/accessibility-handbook/tables/", 73 | "nodes" => array( 74 | 0 => array( 75 | "failureSummary" => "Double-check your table and verify that it is being used to communicate tabular information. If it is being used for layout, find another way to organize content that doesn't use tables.", 76 | "target" => array( 77 | 0 => null, 78 | ) 79 | ) 80 | ), 81 | "description" => "Tables should be used for displaying data or otherwise communicating information that can be sorted into labeled rows and columns. Tables should not be used for layout or aesthetic purposes.", 82 | "html" => null, 83 | ), 84 | "ncsu_captcha" => array( 85 | "id" => "ncsu_captcha", 86 | "impact" => "info", 87 | "help" => "CAPTCHAs can be an accessibility barrier. Avoid using them whenever possible.", 88 | "helpUrl" => "http://accessibility.psu.edu/captcha/", 89 | "nodes" => array( 90 | 0 => array( 91 | "failureSummary" => "This CAPTCHA has likely been added by a plugin. Check the plugin options (or ask a site administrator to do so) to see if there is an alternative to using a CAPTCHA.", 92 | "target" => array( 93 | 0 => null, 94 | ) 95 | ) 96 | ), 97 | "description" => "While some CAPTCHAs can be made accessible, many present a barrier for users with disabilities and create a poor user experience in general. Consider alternatives to CAPTCHAs whenever possible.", 98 | "html" => null, 99 | ), 100 | "ncsu_ambiguous_link" => array( 101 | "id" => "ncsu_ambiguous_link", 102 | "impact" => "info", 103 | "help" => "Make sure your link text is unambiguous and makes sense out of context.", 104 | "helpUrl" => "https://www.wuhcag.com/link-purpose-link-only/", 105 | "nodes" => array( 106 | 0 => array( 107 | "failureSummary" => "Our scan has detected words often associated with ambiguous link text, eg. 'click here' or 'download now.' When phrases like those are the only words in the link, the meaning of the link is unclear. Double-check your link text and avoid these kinds of link phrasings. For example, 'Click here to view examples of our work' (with 'click here' as the link and 'to view examples of our work' as context) could be rewritten simply as 'View examples of our work' (with the entire phrase as part of the link).", 108 | "target" => array( 109 | 0 => null, 110 | ) 111 | ) 112 | ), 113 | "description" => "Some screen reader users scan a web page by skipping between links, and the text of each link is announced without the context of the paragraph around it. The text of the link should be unambiguous and should clearly indicate its destination or its purpose.", 114 | "html" => null, 115 | ) 116 | ); -------------------------------------------------------------------------------- /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 | {$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 | } --------------------------------------------------------------------------------