Make WordPress Core

Changeset 41851


Ignore:
Timestamp:
10/13/2017 02:38:19 AM (7 years ago)
Author:
westonruter
Message:

File Editors: Display list of theme/plugin files in scrollable directory tree.

Props WraithKenny, afercia, melchoyce, westonruter.
Amends [41721].
Fixes #24048.

Location:
trunk/src/wp-admin
Files:
5 edited

Legend:

Unmodified
Added
Removed
  • trunk/src/wp-admin/css/common.css

    r41836 r41851  
    14671467    display: block;
    14681468    margin: 0;
    1469     padding: 5px 12px;
     1469    padding: 5px 8px;
    14701470    font-weight: 600;
    14711471    text-decoration: none;
     
    14731473
    14741474.wrap #templateside span.notice {
    1475   margin-left: -12px;
     1475    margin-left: -12px;
    14761476}
    14771477
    14781478#templateside li.notice a {
    1479   padding: 0;
     1479    padding: 0;
    14801480}
    14811481
     
    30373037    height: calc( 100vh - 280px );
    30383038}
    3039 #templateside {
    3040     margin-top: 31px;
    3041     overflow: scroll;
    3042 }
     3039
     3040#templateside > h2 {
     3041    padding-top: 6px;
     3042    padding-bottom: 6px;
     3043    margin: 0;
     3044    border-bottom: solid 1px #ccc;
     3045}
     3046
     3047#templateside ol,
     3048#templateside ul {
     3049    margin: .5em 0;
     3050    padding: 0;
     3051}
     3052#templateside > ul {
     3053    margin-top: 0;
     3054    overflow: auto;
     3055    padding: 2px;
     3056    height: calc(100vh - 280px);
     3057}
     3058#templateside ul ul {
     3059    padding-left: 12px;
     3060}
     3061
     3062/*
     3063 * Styles for Theme and Plugin editors.
     3064 */
     3065
     3066/* Hide collapsed items. */
     3067[role="treeitem"][aria-expanded="false"] > ul {
     3068    display: none;
     3069}
     3070
     3071/* Use arrow dashicons for folder states, but hide from screen readers. */
     3072[role="treeitem"] span[aria-hidden] {
     3073    display: inline;
     3074    font-family: dashicons;
     3075    font-size: 20px;
     3076    position: absolute;
     3077    pointer-events: none;
     3078}
     3079[role="treeitem"][aria-expanded="false"] > .folder-label .icon:after {
     3080    content: "\f139";
     3081}
     3082[role="treeitem"][aria-expanded="true"] > .folder-label .icon:after {
     3083    content: "\f140";
     3084}
     3085[role="treeitem"] .folder-label {
     3086    display: block;
     3087    padding: 3px 3px 3px 12px;
     3088    cursor: pointer;
     3089}
     3090
     3091/* Remove outline, and create our own focus and hover styles */
     3092[role="treeitem"] {
     3093    outline: 0;
     3094}
     3095[role="treeitem"] .folder-label.focus {
     3096    color: #124964;
     3097    box-shadow: 0 0 0 1px #5b9dd9, 0 0 2px 1px rgba(30, 140, 190, .8);
     3098}
     3099[role="treeitem"].hover,
     3100[role="treeitem"] .folder-label.hover {
     3101    background-color: #DDDDDD;
     3102}
     3103
     3104.tree-folder {
     3105    margin: 0;
     3106    position: relative;
     3107}
     3108[role="treeitem"] li {
     3109    position: relative;
     3110}
     3111
     3112/* Styles for folder indicators/depth */
     3113.tree-folder .tree-folder::after {
     3114    content: ' ';
     3115    display: block;
     3116    position: absolute;
     3117    left: 2px;
     3118    border-left: 1px solid #ccc;
     3119    top: -13px;
     3120    bottom: 10px;
     3121}
     3122.tree-folder > li::before {
     3123    content: ' ';
     3124    position: absolute;
     3125    display: block;
     3126    border-left: 1px solid #ccc;
     3127    left: 2px;
     3128    top: -5px;
     3129    height: 18px;
     3130    width: 7px;
     3131    border-bottom: 1px solid #ccc;
     3132}
     3133.tree-folder > li::after {
     3134    content: ' ';
     3135    position: absolute;
     3136    display: block;
     3137    border-left: 1px solid #ccc;
     3138    left: 2px;
     3139    bottom: -7px;
     3140    top: 0;
     3141}
     3142
     3143/* current-file needs to adjustment for .notice styles */
     3144#templateside .current-file {
     3145    margin: -4px 0 -2px;
     3146}
     3147.tree-folder > .current-file::before {
     3148    left: 4px;
     3149    height: 15px;
     3150    width: 6px;
     3151    border-left: none;
     3152    top: 3px;
     3153}
     3154.tree-folder > .current-file::after {
     3155    bottom: -4px;
     3156    height: 7px;
     3157    left: 2px;
     3158    top: auto;
     3159}
     3160
     3161/* Lines shouldn't continue on last item */
     3162.tree-folder > li:last-child::after,
     3163.tree-folder li:last-child > .tree-folder::after {
     3164    display: none;
     3165}
     3166
    30433167
    30443168#theme-plugin-editor-label {
     
    30913215}
    30923216
    3093 #templateside h2,
    30943217#postcustomstuff p.submit {
    30953218    margin: 0;
     
    30983221#templateside h4 {
    30993222    margin: 1em 0 0;
    3100 }
    3101 
    3102 #templateside ol,
    3103 #templateside ul {
    3104     margin: .5em 0;
    3105     padding: 0;
    31063223}
    31073224
     
    36543771    }
    36553772
     3773    #templateside ul ul {
     3774        padding-left: 1.5em;
     3775    }
     3776    [role="treeitem"] .folder-label {
     3777        display: block;
     3778        padding: 5px;
     3779    }
     3780    .tree-folder > li::before,
     3781    .tree-folder > li::after,
     3782    .tree-folder .tree-folder::after {
     3783        left: -8px;
     3784    }
     3785    .tree-folder > li::before {
     3786        top: 0px;
     3787        height: 13px;
     3788    }
     3789    .tree-folder > .current-file::before {
     3790        left: -5px;
     3791        top: 7px;
     3792        width: 4px;
     3793    }
     3794    .tree-folder > .current-file::after {
     3795        height: 9px;
     3796        left: -8px;
     3797    }
     3798    .wrap #templateside span.notice {
     3799        margin-left: -14px;
     3800    }
     3801
    36563802    .fileedit-sub .alignright {
    36573803        margin-top: 15px;
  • trunk/src/wp-admin/includes/misc.php

    r41741 r41851  
    268268    }
    269269    update_option( 'recently_edited', $oldfiles );
     270}
     271
     272/**
     273 * Makes a tree structure for the Theme Editor's file list.
     274 *
     275 * @since 4.9.0
     276 * @access private
     277 *
     278 * @param array $allowed_files List of theme file paths.
     279 * @return array Tree structure for listing theme files.
     280 */
     281function wp_make_theme_file_tree( $allowed_files ) {
     282    $tree_list = array();
     283    foreach ( $allowed_files as $file_name => $absolute_filename ) {
     284        $list = explode( '/', $file_name );
     285        $last_dir = &$tree_list;
     286        foreach ( $list as $dir ) {
     287            $last_dir =& $last_dir[ $dir ];
     288        }
     289        $last_dir = $file_name;
     290    }
     291    return $tree_list;
     292}
     293
     294/**
     295 * Outputs the formatted file list for the Theme Editor.
     296 *
     297 * @since 4.9.0
     298 * @access private
     299 *
     300 * @param array|string $tree  List of file/folder paths, or filename.
     301 * @param int          $level The aria-level for the current iteration.
     302 * @param int          $size  The aria-setsize for the current iteration.
     303 * @param int          $index The aria-posinset for the current iteration.
     304 */
     305function wp_print_theme_file_tree( $tree, $level = 2, $size = 1, $index = 1 ) {
     306    global $relative_file, $stylesheet;
     307
     308    if ( is_array( $tree ) ) {
     309        $index = 0;
     310        $size = count( $tree );
     311        foreach ( $tree as $label => $theme_file ) :
     312            $index++;
     313            if ( ! is_array( $theme_file ) ) {
     314                wp_print_theme_file_tree( $theme_file, $level, $index, $size );
     315                continue;
     316            }
     317            ?>
     318            <li role="treeitem" aria-expanded="true" tabindex="-1"
     319                aria-level="<?php echo esc_attr( $level ); ?>"
     320                aria-setsize="<?php echo esc_attr( $size ); ?>"
     321                aria-posinset="<?php echo esc_attr( $index ); ?>">
     322                <span class="folder-label"><?php echo esc_html( $label ); ?> <span class="screen-reader-text"><?php _e( 'folder' ); ?></span><span aria-hidden="true" class="icon"></span></span>
     323                <ul role="group" class="tree-folder"><?php wp_print_theme_file_tree( $theme_file, $level + 1, $index, $size ); ?></ul>
     324            </li>
     325            <?php
     326        endforeach;
     327    } else {
     328        $filename = $tree;
     329        $url = add_query_arg(
     330            array(
     331                'file' => rawurlencode( $tree ),
     332                'theme' => rawurlencode( $stylesheet ),
     333            ),
     334            admin_url( 'theme-editor.php' )
     335        );
     336        ?>
     337        <li role="none" class="<?php echo esc_attr( $relative_file === $filename ? 'current-file' : '' ); ?>">
     338            <a role="treeitem" tabindex="<?php echo esc_attr( $relative_file === $filename ? '0' : '-1' ); ?>"
     339                href="<?php echo esc_url( $url ); ?>"
     340                aria-level="<?php echo esc_attr( $level ); ?>"
     341                aria-setsize="<?php echo esc_attr( $size ); ?>"
     342                aria-posinset="<?php echo esc_attr( $index ); ?>">
     343                <?php
     344                $file_description = esc_html( get_file_description( $filename ) );
     345                if ( $file_description !== $filename && basename( $filename ) !== $file_description ) {
     346                    $file_description .= '<br /><span class="nonessential">(' . esc_html( $filename ) . ')</span>';
     347                }
     348
     349                if ( $relative_file === $filename ) {
     350                    echo '<span class="notice notice-info">' . $file_description . '</span>';
     351                } else {
     352                    echo $file_description;
     353                }
     354                ?>
     355            </a>
     356        </li>
     357        <?php
     358    }
     359}
     360
     361/**
     362 * Makes a tree structure for the Plugin Editor's file list.
     363 *
     364 * @since 4.9.0
     365 * @access private
     366 *
     367 * @param string $plugin_editable_files List of plugin file paths.
     368 * @return array Tree structure for listing plugin files.
     369 */
     370function wp_make_plugin_file_tree( $plugin_editable_files ) {
     371    $tree_list = array();
     372    foreach ( $plugin_editable_files as $plugin_file ) {
     373        $list = explode( '/', preg_replace( '#^.+?/#', '', $plugin_file ) );
     374        $last_dir = &$tree_list;
     375        foreach ( $list as $dir ) {
     376            $last_dir =& $last_dir[ $dir ];
     377        }
     378        $last_dir = $plugin_file;
     379    }
     380    return $tree_list;
     381}
     382
     383/**
     384 * Outputs the formatted file list for the Plugin Editor.
     385 *
     386 * @since 4.9.0
     387 * @access private
     388 *
     389 * @param array|string $tree  List of file/folder paths, or filename.
     390 * @param string       $label Name of file or folder to print.
     391 * @param int          $level The aria-level for the current iteration.
     392 * @param int          $size  The aria-setsize for the current iteration.
     393 * @param int          $index The aria-posinset for the current iteration.
     394 */
     395function wp_print_plugin_file_tree( $tree, $label = '', $level = 2, $size = 1, $index = 1 ) {
     396    global $file, $plugin;
     397    if ( is_array( $tree ) ) {
     398        $index = 0;
     399        $size = count( $tree );
     400        foreach ( $tree as $label => $plugin_file ) :
     401            $index++;
     402            if ( ! is_array( $plugin_file ) ) {
     403                wp_print_plugin_file_tree( $plugin_file, $label, $level, $index, $size );
     404                continue;
     405            }
     406            ?>
     407            <li role="treeitem" aria-expanded="true" tabindex="-1"
     408                aria-level="<?php echo esc_attr( $level ); ?>"
     409                aria-setsize="<?php echo esc_attr( $size ); ?>"
     410                aria-posinset="<?php echo esc_attr( $index ); ?>">
     411                <span class="folder-label"><?php echo esc_html( $label ); ?> <span class="screen-reader-text"><?php _e( 'folder' ); ?></span><span aria-hidden="true" class="icon"></span></span>
     412                <ul role="group" class="tree-folder"><?php wp_print_plugin_file_tree( $plugin_file, '', $level + 1, $index, $size ); ?></ul>
     413            </li>
     414            <?php
     415        endforeach;
     416    } else {
     417        $url = add_query_arg(
     418            array(
     419                'file' => rawurlencode( $tree ),
     420                'plugin' => rawurlencode( $plugin ),
     421            ),
     422            admin_url( 'plugin-editor.php' )
     423        );
     424        ?>
     425        <li role="none" class="<?php echo esc_attr( $file === $tree ? 'current-file' : '' ); ?>">
     426            <a role="treeitem" tabindex="<?php echo esc_attr( $file === $tree ? '0' : '-1' ); ?>"
     427                href="<?php echo esc_url( $url ); ?>"
     428                aria-level="<?php echo esc_attr( $level ); ?>"
     429                aria-setsize="<?php echo esc_attr( $size ); ?>"
     430                aria-posinset="<?php echo esc_attr( $index ); ?>">
     431                <?php
     432                if ( $file === $tree ) {
     433                    echo '<span class="notice notice-info">' . esc_html( $label ) . '</span>';
     434                } else {
     435                    echo esc_html( $label );
     436                }
     437                ?>
     438            </a>
     439        </li>
     440        <?php
     441    }
    270442}
    271443
  • trunk/src/wp-admin/js/theme-plugin-editor.js

    r41805 r41851  
    77wp.themePluginEditor = (function( $ ) {
    88    'use strict';
    9 
    10     var component = {
     9    var component, TreeLinks;
     10
     11    component = {
    1112        l10n: {
    1213            lintError: {
     
    6768        }
    6869
     70        $( component.initFileBrowser );
     71
    6972        $( window ).on( 'beforeunload', function() {
    7073            if ( component.dirty ) {
     
    8790        });
    8891
    89         // hide modal
     92        // Hide modal.
    9093        component.warning.remove();
    9194        $( 'body' ).removeClass( 'modal-open' );
    9295
    93         // return focus - is this a trap?
     96        // Return focus - is this a trap?
    9497        component.instance.codemirror.focus();
    9598    };
     
    149152        }
    150153
    151         request.done( function ( response ) {
     154        request.done( function( response ) {
    152155            component.lastSaveNoticeCode = 'file_saved';
    153156            component.addNotice({
     
    160163        } );
    161164
    162         request.fail( function ( response ) {
     165        request.fail( function( response ) {
    163166            var notice = $.extend(
    164167                {
     
    351354    };
    352355
     356    /**
     357     * Initialization of the file browser's folder states.
     358     *
     359     * @since 4.9.0
     360     * @returns {void}
     361     */
     362    component.initFileBrowser = function initFileBrowser() {
     363
     364        var $templateside = $( '#templateside' );
     365
     366        // Collapse all folders.
     367        $templateside.find( '[role="group"]' ).parent().attr( 'aria-expanded', false );
     368
     369        // Expand ancestors to the current file.
     370        $templateside.find( '.notice' ).parents( '[aria-expanded]' ).attr( 'aria-expanded', true );
     371
     372        // Find Tree elements and enhance them.
     373        $templateside.find( '[role="tree"]' ).each( function() {
     374            var treeLinks = new TreeLinks( this );
     375            treeLinks.init();
     376        } );
     377
     378        // Scroll the current file into view.
     379        $templateside.find( '.current-file' ).each( function() {
     380            this.scrollIntoView( false );
     381        } );
     382    };
     383
     384    /* jshint ignore:start */
     385    /* jscs:disable */
     386    /* eslint-disable */
     387
     388    /**
     389     * Creates a new TreeitemLink.
     390     *
     391     * @since 4.9.0
     392     * @class
     393     * @private
     394     * @see {@link https://www.w3.org/TR/wai-aria-practices-1.1/examples/treeview/treeview-2/treeview-2b.html|W3C Treeview Example}
     395     * @license W3C-20150513
     396     */
     397    var TreeitemLink = (function () {
     398        /**
     399         *   This content is licensed according to the W3C Software License at
     400         *   https://www.w3.org/Consortium/Legal/2015/copyright-software-and-document
     401         *
     402         *   File:   TreeitemLink.js
     403         *
     404         *   Desc:   Treeitem widget that implements ARIA Authoring Practices
     405         *           for a tree being used as a file viewer
     406         *
     407         *   Author: Jon Gunderson, Ku Ja Eun and Nicholas Hoyt
     408         */
     409
     410        /**
     411         *   @constructor
     412         *
     413         *   @desc
     414         *       Treeitem object for representing the state and user interactions for a
     415         *       treeItem widget
     416         *
     417         *   @param node
     418         *       An element with the role=tree attribute
     419         */
     420
     421        var TreeitemLink = function (node, treeObj, group) {
     422
     423            // Check whether node is a DOM element
     424            if (typeof node !== 'object') {
     425                return;
     426            }
     427
     428            node.tabIndex = -1;
     429            this.tree = treeObj;
     430            this.groupTreeitem = group;
     431            this.domNode = node;
     432            this.label = node.textContent.trim();
     433            this.stopDefaultClick = false;
     434
     435            if (node.getAttribute('aria-label')) {
     436                this.label = node.getAttribute('aria-label').trim();
     437            }
     438
     439            this.isExpandable = false;
     440            this.isVisible = false;
     441            this.inGroup = false;
     442
     443            if (group) {
     444                this.inGroup = true;
     445            }
     446
     447            var elem = node.firstElementChild;
     448
     449            while (elem) {
     450
     451                if (elem.tagName.toLowerCase() == 'ul') {
     452                    elem.setAttribute('role', 'group');
     453                    this.isExpandable = true;
     454                    break;
     455                }
     456
     457                elem = elem.nextElementSibling;
     458            }
     459
     460            this.keyCode = Object.freeze({
     461                RETURN: 13,
     462                SPACE: 32,
     463                PAGEUP: 33,
     464                PAGEDOWN: 34,
     465                END: 35,
     466                HOME: 36,
     467                LEFT: 37,
     468                UP: 38,
     469                RIGHT: 39,
     470                DOWN: 40
     471            });
     472        };
     473
     474        TreeitemLink.prototype.init = function () {
     475            this.domNode.tabIndex = -1;
     476
     477            if (!this.domNode.getAttribute('role')) {
     478                this.domNode.setAttribute('role', 'treeitem');
     479            }
     480
     481            this.domNode.addEventListener('keydown', this.handleKeydown.bind(this));
     482            this.domNode.addEventListener('click', this.handleClick.bind(this));
     483            this.domNode.addEventListener('focus', this.handleFocus.bind(this));
     484            this.domNode.addEventListener('blur', this.handleBlur.bind(this));
     485
     486            if (this.isExpandable) {
     487                this.domNode.firstElementChild.addEventListener('mouseover', this.handleMouseOver.bind(this));
     488                this.domNode.firstElementChild.addEventListener('mouseout', this.handleMouseOut.bind(this));
     489            }
     490            else {
     491                this.domNode.addEventListener('mouseover', this.handleMouseOver.bind(this));
     492                this.domNode.addEventListener('mouseout', this.handleMouseOut.bind(this));
     493            }
     494        };
     495
     496        TreeitemLink.prototype.isExpanded = function () {
     497
     498            if (this.isExpandable) {
     499                return this.domNode.getAttribute('aria-expanded') === 'true';
     500            }
     501
     502            return false;
     503
     504        };
     505
     506        /* EVENT HANDLERS */
     507
     508        TreeitemLink.prototype.handleKeydown = function (event) {
     509            var tgt = event.currentTarget,
     510                flag = false,
     511                _char = event.key,
     512                clickEvent;
     513
     514            function isPrintableCharacter(str) {
     515                return str.length === 1 && str.match(/\S/);
     516            }
     517
     518            function printableCharacter(item) {
     519                if (_char == '*') {
     520                    item.tree.expandAllSiblingItems(item);
     521                    flag = true;
     522                }
     523                else {
     524                    if (isPrintableCharacter(_char)) {
     525                        item.tree.setFocusByFirstCharacter(item, _char);
     526                        flag = true;
     527                    }
     528                }
     529            }
     530
     531            this.stopDefaultClick = false;
     532
     533            if (event.altKey || event.ctrlKey || event.metaKey) {
     534                return;
     535            }
     536
     537            if (event.shift) {
     538                if (event.keyCode == this.keyCode.SPACE || event.keyCode == this.keyCode.RETURN) {
     539                    event.stopPropagation();
     540                    this.stopDefaultClick = true;
     541                }
     542                else {
     543                    if (isPrintableCharacter(_char)) {
     544                        printableCharacter(this);
     545                    }
     546                }
     547            }
     548            else {
     549                switch (event.keyCode) {
     550                    case this.keyCode.SPACE:
     551                    case this.keyCode.RETURN:
     552                        if (this.isExpandable) {
     553                            if (this.isExpanded()) {
     554                                this.tree.collapseTreeitem(this);
     555                            }
     556                            else {
     557                                this.tree.expandTreeitem(this);
     558                            }
     559                            flag = true;
     560                        }
     561                        else {
     562                            event.stopPropagation();
     563                            this.stopDefaultClick = true;
     564                        }
     565                        break;
     566
     567                    case this.keyCode.UP:
     568                        this.tree.setFocusToPreviousItem(this);
     569                        flag = true;
     570                        break;
     571
     572                    case this.keyCode.DOWN:
     573                        this.tree.setFocusToNextItem(this);
     574                        flag = true;
     575                        break;
     576
     577                    case this.keyCode.RIGHT:
     578                        if (this.isExpandable) {
     579                            if (this.isExpanded()) {
     580                                this.tree.setFocusToNextItem(this);
     581                            }
     582                            else {
     583                                this.tree.expandTreeitem(this);
     584                            }
     585                        }
     586                        flag = true;
     587                        break;
     588
     589                    case this.keyCode.LEFT:
     590                        if (this.isExpandable && this.isExpanded()) {
     591                            this.tree.collapseTreeitem(this);
     592                            flag = true;
     593                        }
     594                        else {
     595                            if (this.inGroup) {
     596                                this.tree.setFocusToParentItem(this);
     597                                flag = true;
     598                            }
     599                        }
     600                        break;
     601
     602                    case this.keyCode.HOME:
     603                        this.tree.setFocusToFirstItem();
     604                        flag = true;
     605                        break;
     606
     607                    case this.keyCode.END:
     608                        this.tree.setFocusToLastItem();
     609                        flag = true;
     610                        break;
     611
     612                    default:
     613                        if (isPrintableCharacter(_char)) {
     614                            printableCharacter(this);
     615                        }
     616                        break;
     617                }
     618            }
     619
     620            if (flag) {
     621                event.stopPropagation();
     622                event.preventDefault();
     623            }
     624        };
     625
     626        TreeitemLink.prototype.handleClick = function (event) {
     627
     628            // only process click events that directly happened on this treeitem
     629            if (event.target !== this.domNode && event.target !== this.domNode.firstElementChild) {
     630                return;
     631            }
     632
     633            if (this.isExpandable) {
     634                if (this.isExpanded()) {
     635                    this.tree.collapseTreeitem(this);
     636                }
     637                else {
     638                    this.tree.expandTreeitem(this);
     639                }
     640                event.stopPropagation();
     641            }
     642        };
     643
     644        TreeitemLink.prototype.handleFocus = function (event) {
     645            var node = this.domNode;
     646            if (this.isExpandable) {
     647                node = node.firstElementChild;
     648            }
     649            node.classList.add('focus');
     650        };
     651
     652        TreeitemLink.prototype.handleBlur = function (event) {
     653            var node = this.domNode;
     654            if (this.isExpandable) {
     655                node = node.firstElementChild;
     656            }
     657            node.classList.remove('focus');
     658        };
     659
     660        TreeitemLink.prototype.handleMouseOver = function (event) {
     661            event.currentTarget.classList.add('hover');
     662        };
     663
     664        TreeitemLink.prototype.handleMouseOut = function (event) {
     665            event.currentTarget.classList.remove('hover');
     666        };
     667
     668        return TreeitemLink;
     669    })();
     670
     671    /**
     672     * Creates a new TreeLinks.
     673     *
     674     * @since 4.9.0
     675     * @class
     676     * @private
     677     * @see {@link https://www.w3.org/TR/wai-aria-practices-1.1/examples/treeview/treeview-2/treeview-2b.html|W3C Treeview Example}
     678     * @license W3C-20150513
     679     */
     680    TreeLinks = (function () {
     681        /*
     682         *   This content is licensed according to the W3C Software License at
     683         *   https://www.w3.org/Consortium/Legal/2015/copyright-software-and-document
     684         *
     685         *   File:   TreeLinks.js
     686         *
     687         *   Desc:   Tree widget that implements ARIA Authoring Practices
     688         *           for a tree being used as a file viewer
     689         *
     690         *   Author: Jon Gunderson, Ku Ja Eun and Nicholas Hoyt
     691         */
     692
     693        /*
     694         *   @constructor
     695         *
     696         *   @desc
     697         *       Tree item object for representing the state and user interactions for a
     698         *       tree widget
     699         *
     700         *   @param node
     701         *       An element with the role=tree attribute
     702         */
     703
     704        var TreeLinks = function (node) {
     705            // Check whether node is a DOM element
     706            if (typeof node !== 'object') {
     707                return;
     708            }
     709
     710            this.domNode = node;
     711
     712            this.treeitems = [];
     713            this.firstChars = [];
     714
     715            this.firstTreeitem = null;
     716            this.lastTreeitem = null;
     717
     718        };
     719
     720        TreeLinks.prototype.init = function () {
     721
     722            function findTreeitems(node, tree, group) {
     723
     724                var elem = node.firstElementChild;
     725                var ti = group;
     726
     727                while (elem) {
     728
     729                    if ((elem.tagName.toLowerCase() === 'li' && elem.firstElementChild.tagName.toLowerCase() === 'span') || elem.tagName.toLowerCase() === 'a') {
     730                        ti = new TreeitemLink(elem, tree, group);
     731                        ti.init();
     732                        tree.treeitems.push(ti);
     733                        tree.firstChars.push(ti.label.substring(0, 1).toLowerCase());
     734                    }
     735
     736                    if (elem.firstElementChild) {
     737                        findTreeitems(elem, tree, ti);
     738                    }
     739
     740                    elem = elem.nextElementSibling;
     741                }
     742            }
     743
     744            // initialize pop up menus
     745            if (!this.domNode.getAttribute('role')) {
     746                this.domNode.setAttribute('role', 'tree');
     747            }
     748
     749            findTreeitems(this.domNode, this, false);
     750
     751            this.updateVisibleTreeitems();
     752
     753            this.firstTreeitem.domNode.tabIndex = 0;
     754
     755        };
     756
     757        TreeLinks.prototype.setFocusToItem = function (treeitem) {
     758
     759            for (var i = 0; i < this.treeitems.length; i++) {
     760                var ti = this.treeitems[i];
     761
     762                if (ti === treeitem) {
     763                    ti.domNode.tabIndex = 0;
     764                    ti.domNode.focus();
     765                }
     766                else {
     767                    ti.domNode.tabIndex = -1;
     768                }
     769            }
     770
     771        };
     772
     773        TreeLinks.prototype.setFocusToNextItem = function (currentItem) {
     774
     775            var nextItem = false;
     776
     777            for (var i = (this.treeitems.length - 1); i >= 0; i--) {
     778                var ti = this.treeitems[i];
     779                if (ti === currentItem) {
     780                    break;
     781                }
     782                if (ti.isVisible) {
     783                    nextItem = ti;
     784                }
     785            }
     786
     787            if (nextItem) {
     788                this.setFocusToItem(nextItem);
     789            }
     790
     791        };
     792
     793        TreeLinks.prototype.setFocusToPreviousItem = function (currentItem) {
     794
     795            var prevItem = false;
     796
     797            for (var i = 0; i < this.treeitems.length; i++) {
     798                var ti = this.treeitems[i];
     799                if (ti === currentItem) {
     800                    break;
     801                }
     802                if (ti.isVisible) {
     803                    prevItem = ti;
     804                }
     805            }
     806
     807            if (prevItem) {
     808                this.setFocusToItem(prevItem);
     809            }
     810        };
     811
     812        TreeLinks.prototype.setFocusToParentItem = function (currentItem) {
     813
     814            if (currentItem.groupTreeitem) {
     815                this.setFocusToItem(currentItem.groupTreeitem);
     816            }
     817        };
     818
     819        TreeLinks.prototype.setFocusToFirstItem = function () {
     820            this.setFocusToItem(this.firstTreeitem);
     821        };
     822
     823        TreeLinks.prototype.setFocusToLastItem = function () {
     824            this.setFocusToItem(this.lastTreeitem);
     825        };
     826
     827        TreeLinks.prototype.expandTreeitem = function (currentItem) {
     828
     829            if (currentItem.isExpandable) {
     830                currentItem.domNode.setAttribute('aria-expanded', true);
     831                this.updateVisibleTreeitems();
     832            }
     833
     834        };
     835
     836        TreeLinks.prototype.expandAllSiblingItems = function (currentItem) {
     837            for (var i = 0; i < this.treeitems.length; i++) {
     838                var ti = this.treeitems[i];
     839
     840                if ((ti.groupTreeitem === currentItem.groupTreeitem) && ti.isExpandable) {
     841                    this.expandTreeitem(ti);
     842                }
     843            }
     844
     845        };
     846
     847        TreeLinks.prototype.collapseTreeitem = function (currentItem) {
     848
     849            var groupTreeitem = false;
     850
     851            if (currentItem.isExpanded()) {
     852                groupTreeitem = currentItem;
     853            }
     854            else {
     855                groupTreeitem = currentItem.groupTreeitem;
     856            }
     857
     858            if (groupTreeitem) {
     859                groupTreeitem.domNode.setAttribute('aria-expanded', false);
     860                this.updateVisibleTreeitems();
     861                this.setFocusToItem(groupTreeitem);
     862            }
     863
     864        };
     865
     866        TreeLinks.prototype.updateVisibleTreeitems = function () {
     867
     868            this.firstTreeitem = this.treeitems[0];
     869
     870            for (var i = 0; i < this.treeitems.length; i++) {
     871                var ti = this.treeitems[i];
     872
     873                var parent = ti.domNode.parentNode;
     874
     875                ti.isVisible = true;
     876
     877                while (parent && (parent !== this.domNode)) {
     878
     879                    if (parent.getAttribute('aria-expanded') == 'false') {
     880                        ti.isVisible = false;
     881                    }
     882                    parent = parent.parentNode;
     883                }
     884
     885                if (ti.isVisible) {
     886                    this.lastTreeitem = ti;
     887                }
     888            }
     889
     890        };
     891
     892        TreeLinks.prototype.setFocusByFirstCharacter = function (currentItem, _char) {
     893            var start, index;
     894            _char = _char.toLowerCase();
     895
     896            // Get start index for search based on position of currentItem
     897            start = this.treeitems.indexOf(currentItem) + 1;
     898            if (start === this.treeitems.length) {
     899                start = 0;
     900            }
     901
     902            // Check remaining slots in the menu
     903            index = this.getIndexFirstChars(start, _char);
     904
     905            // If not found in remaining slots, check from beginning
     906            if (index === -1) {
     907                index = this.getIndexFirstChars(0, _char);
     908            }
     909
     910            // If match was found...
     911            if (index > -1) {
     912                this.setFocusToItem(this.treeitems[index]);
     913            }
     914        };
     915
     916        TreeLinks.prototype.getIndexFirstChars = function (startIndex, _char) {
     917            for (var i = startIndex; i < this.firstChars.length; i++) {
     918                if (this.treeitems[i].isVisible) {
     919                    if (_char === this.firstChars[i]) {
     920                        return i;
     921                    }
     922                }
     923            }
     924            return -1;
     925        };
     926
     927        return TreeLinks;
     928    })();
     929
     930    /* jshint ignore:end */
     931    /* jscs:enable */
     932    /* eslint-enable */
     933
    353934    return component;
    354935})( jQuery );
  • trunk/src/wp-admin/plugin-editor.php

    r41774 r41851  
    232232
    233233<div id="templateside">
    234     <h2><?php _e( 'Plugin Files' ); ?></h2>
     234    <h2 id="plugin-files-label"><?php _e( 'Plugin Files' ); ?></h2>
    235235
    236236    <?php
     
    242242    }
    243243    ?>
    244     <ul>
    245     <?php foreach ( $plugin_editable_files as $plugin_file ) : ?>
    246         <li class="<?php echo esc_attr( $file === $plugin_file ? 'notice notice-info' : '' ); ?>">
    247             <a href="plugin-editor.php?file=<?php echo urlencode( $plugin_file ); ?>&amp;plugin=<?php echo urlencode( $plugin ); ?>"><?php echo esc_html( preg_replace( '#^.+?/#', '', $plugin_file ) ); ?></a>
    248         </li>
    249     <?php endforeach; ?>
     244    <ul role="tree" aria-labelledby="plugin-files-label">
     245    <li role="treeitem" tabindex="-1" aria-expanded="true"
     246        aria-level="1"
     247        aria-posinset="1"
     248        aria-setsize="1">
     249        <ul role="group" style="padding-left: 0;">
     250            <?php wp_print_plugin_file_tree( wp_make_plugin_file_tree( $plugin_editable_files ) ); ?>
     251        </ul>
    250252    </ul>
    251253</div>
  • trunk/src/wp-admin/theme-editor.php

    r41806 r41851  
    8888            break;
    8989    }
     90}
     91
     92// Move functions.php and style.css to the top.
     93if ( isset( $allowed_files['functions.php'] ) ) {
     94    $allowed_files = array( 'functions.php' => $allowed_files['functions.php'] ) + $allowed_files;
     95}
     96if ( isset( $allowed_files['style.css'] ) ) {
     97    $allowed_files = array( 'style.css' => $allowed_files['style.css'] ) + $allowed_files;
    9098}
    9199
     
    206214    echo '<div class="error"><p><strong>' . __( 'This theme is broken.' ) . '</strong> ' . $theme->errors()->get_error_message() . '</p></div>';
    207215?>
    208     <div id="templateside">
    209 <?php
    210 if ( $allowed_files ) :
    211     $previous_file_type = '';
    212 
    213     foreach ( $allowed_files as $filename => $absolute_filename ) :
    214         $file_type = substr( $filename, strrpos( $filename, '.' ) );
    215 
    216         if ( $file_type !== $previous_file_type ) {
    217             if ( '' !== $previous_file_type ) {
    218                 echo "\t</ul>\n";
    219             }
    220 
    221             switch ( $file_type ) {
    222                 case '.php':
    223                     if ( $has_templates || $theme->parent() ) :
    224                         echo "\t<h2>" . __( 'Templates' ) . "</h2>\n";
    225                         if ( $theme->parent() ) {
    226                             echo '<p class="howto">' . sprintf( __( 'This child theme inherits templates from a parent theme, %s.' ),
    227                                 sprintf( '<a href="%s">%s</a>',
    228                                     self_admin_url( 'theme-editor.php?theme=' . urlencode( $theme->get_template() ) ),
    229                                     $theme->parent()->display( 'Name' )
    230                                 )
    231                             ) . "</p>\n";
    232                         }
    233                     endif;
    234                     break;
    235                 case '.css':
    236                     echo "\t<h2>" . _x( 'Styles', 'Theme stylesheets in theme editor' ) . "</h2>\n";
    237                     break;
    238                 default:
    239                     /* translators: %s: file extension */
    240                     echo "\t<h2>" . sprintf( __( '%s files' ), $file_type ) . "</h2>\n";
    241                     break;
    242             }
    243 
    244             echo "\t<ul>\n";
     216<div id="templateside">
     217    <h2 id="theme-files-label"><?php _e( 'Theme Files' ); ?></h2>
     218    <?php
     219    if ( $has_templates || $theme->parent() ) :
     220        if ( $theme->parent() ) {
     221            /* translators: %s: link to edit parent theme */
     222            echo '<p class="howto">' . sprintf( __( 'This child theme inherits templates from a parent theme, %s.' ),
     223                sprintf( '<a href="%s">%s</a>',
     224                    self_admin_url( 'theme-editor.php?theme=' . urlencode( $theme->get_template() ) ),
     225                    $theme->parent()->display( 'Name' )
     226                )
     227            ) . "</p>\n";
    245228        }
    246 
    247         $file_description = esc_html( get_file_description( $filename ) );
    248         if ( $filename !== basename( $absolute_filename ) || $file_description !== $filename ) {
    249             $file_description .= '<br /><span class="nonessential">(' . esc_html( $filename ) . ')</span>';
    250         }
    251 
    252         if ( $absolute_filename === $file ) {
    253             $file_description = '<span class="notice notice-info">' . $file_description . '</span>';
    254         }
    255 
    256         $previous_file_type = $file_type;
    257 ?>
    258         <li><a href="theme-editor.php?file=<?php echo urlencode( $filename ) ?>&amp;theme=<?php echo urlencode( $stylesheet ) ?>"><?php echo $file_description; ?></a></li>
    259 <?php
    260     endforeach;
    261 ?>
    262 </ul>
    263 <?php endif; ?>
    264 </div>
     229    endif;
     230    ?>
     231    <ul role="tree" aria-labelledby="theme-files-label">
     232        <li role="treeitem" tabindex="-1" aria-expanded="true"
     233            aria-level="1"
     234            aria-posinset="1"
     235            aria-setsize="1">
     236            <ul role="group" style="padding-left: 0;">
     237                <?php wp_print_theme_file_tree( wp_make_theme_file_tree( $allowed_files ) ); ?>
     238            </ul>
     239        </li>
     240    </ul>
     241</div>
     242
    265243<?php if ( $error ) :
    266244    echo '<div class="error"><p>' . __('Oops, no such file exists! Double check the name and try again, merci.') . '</p></div>';
Note: See TracChangeset for help on using the changeset viewer.