From 8bf9afb4f4a9989c59524e9422bc11eb833e209b Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Fri, 13 Oct 2017 02:38:19 +0000 Subject: [PATCH] File Editors: Display list of theme/plugin files in scrollable directory tree. Props WraithKenny, afercia, melchoyce, westonruter. Amends [41721]. Fixes #24048. git-svn-id: https://develop.svn.wordpress.org/trunk@41851 602fd350-edb4-49c9-b593-d223f7449a82 --- src/wp-admin/css/common.css | 172 ++++++- src/wp-admin/includes/misc.php | 172 +++++++ src/wp-admin/js/theme-plugin-editor.js | 591 ++++++++++++++++++++++++- src/wp-admin/plugin-editor.php | 16 +- src/wp-admin/theme-editor.php | 88 ++-- 5 files changed, 959 insertions(+), 80 deletions(-) diff --git a/src/wp-admin/css/common.css b/src/wp-admin/css/common.css index 1702e7736c..8224b4345d 100644 --- a/src/wp-admin/css/common.css +++ b/src/wp-admin/css/common.css @@ -1466,17 +1466,17 @@ div.error { .wrap #templateside .notice { display: block; margin: 0; - padding: 5px 12px; + padding: 5px 8px; font-weight: 600; text-decoration: none; } .wrap #templateside span.notice { - margin-left: -12px; + margin-left: -12px; } #templateside li.notice a { - padding: 0; + padding: 0; } /* Update icon. */ @@ -3036,11 +3036,135 @@ img { width: 97%; height: calc( 100vh - 280px ); } -#templateside { - margin-top: 31px; - overflow: scroll; + +#templateside > h2 { + padding-top: 6px; + padding-bottom: 6px; + margin: 0; + border-bottom: solid 1px #ccc; } +#templateside ol, +#templateside ul { + margin: .5em 0; + padding: 0; +} +#templateside > ul { + margin-top: 0; + overflow: auto; + padding: 2px; + height: calc(100vh - 280px); +} +#templateside ul ul { + padding-left: 12px; +} + +/* + * Styles for Theme and Plugin editors. + */ + +/* Hide collapsed items. */ +[role="treeitem"][aria-expanded="false"] > ul { + display: none; +} + +/* Use arrow dashicons for folder states, but hide from screen readers. */ +[role="treeitem"] span[aria-hidden] { + display: inline; + font-family: dashicons; + font-size: 20px; + position: absolute; + pointer-events: none; +} +[role="treeitem"][aria-expanded="false"] > .folder-label .icon:after { + content: "\f139"; +} +[role="treeitem"][aria-expanded="true"] > .folder-label .icon:after { + content: "\f140"; +} +[role="treeitem"] .folder-label { + display: block; + padding: 3px 3px 3px 12px; + cursor: pointer; +} + +/* Remove outline, and create our own focus and hover styles */ +[role="treeitem"] { + outline: 0; +} +[role="treeitem"] .folder-label.focus { + color: #124964; + box-shadow: 0 0 0 1px #5b9dd9, 0 0 2px 1px rgba(30, 140, 190, .8); +} +[role="treeitem"].hover, +[role="treeitem"] .folder-label.hover { + background-color: #DDDDDD; +} + +.tree-folder { + margin: 0; + position: relative; +} +[role="treeitem"] li { + position: relative; +} + +/* Styles for folder indicators/depth */ +.tree-folder .tree-folder::after { + content: ' '; + display: block; + position: absolute; + left: 2px; + border-left: 1px solid #ccc; + top: -13px; + bottom: 10px; +} +.tree-folder > li::before { + content: ' '; + position: absolute; + display: block; + border-left: 1px solid #ccc; + left: 2px; + top: -5px; + height: 18px; + width: 7px; + border-bottom: 1px solid #ccc; +} +.tree-folder > li::after { + content: ' '; + position: absolute; + display: block; + border-left: 1px solid #ccc; + left: 2px; + bottom: -7px; + top: 0; +} + +/* current-file needs to adjustment for .notice styles */ +#templateside .current-file { + margin: -4px 0 -2px; +} +.tree-folder > .current-file::before { + left: 4px; + height: 15px; + width: 6px; + border-left: none; + top: 3px; +} +.tree-folder > .current-file::after { + bottom: -4px; + height: 7px; + left: 2px; + top: auto; +} + +/* Lines shouldn't continue on last item */ +.tree-folder > li:last-child::after, +.tree-folder li:last-child > .tree-folder::after { + display: none; +} + + #theme-plugin-editor-label { display: inline-block; margin-bottom: 1em; @@ -3090,7 +3214,6 @@ img { word-wrap: break-word; } -#templateside h2, #postcustomstuff p.submit { margin: 0; } @@ -3099,12 +3222,6 @@ img { margin: 1em 0 0; } -#templateside ol, -#templateside ul { - margin: .5em 0; - padding: 0; -} - #templateside li { margin: 4px 0; } @@ -3653,6 +3770,35 @@ img { width: 100%; } + #templateside ul ul { + padding-left: 1.5em; + } + [role="treeitem"] .folder-label { + display: block; + padding: 5px; + } + .tree-folder > li::before, + .tree-folder > li::after, + .tree-folder .tree-folder::after { + left: -8px; + } + .tree-folder > li::before { + top: 0px; + height: 13px; + } + .tree-folder > .current-file::before { + left: -5px; + top: 7px; + width: 4px; + } + .tree-folder > .current-file::after { + height: 9px; + left: -8px; + } + .wrap #templateside span.notice { + margin-left: -14px; + } + .fileedit-sub .alignright { margin-top: 15px; } diff --git a/src/wp-admin/includes/misc.php b/src/wp-admin/includes/misc.php index c434b8d3ed..5cf0e21f28 100644 --- a/src/wp-admin/includes/misc.php +++ b/src/wp-admin/includes/misc.php @@ -269,6 +269,178 @@ function update_recently_edited( $file ) { update_option( 'recently_edited', $oldfiles ); } +/** + * Makes a tree structure for the Theme Editor's file list. + * + * @since 4.9.0 + * @access private + * + * @param array $allowed_files List of theme file paths. + * @return array Tree structure for listing theme files. + */ +function wp_make_theme_file_tree( $allowed_files ) { + $tree_list = array(); + foreach ( $allowed_files as $file_name => $absolute_filename ) { + $list = explode( '/', $file_name ); + $last_dir = &$tree_list; + foreach ( $list as $dir ) { + $last_dir =& $last_dir[ $dir ]; + } + $last_dir = $file_name; + } + return $tree_list; +} + +/** + * Outputs the formatted file list for the Theme Editor. + * + * @since 4.9.0 + * @access private + * + * @param array|string $tree List of file/folder paths, or filename. + * @param int $level The aria-level for the current iteration. + * @param int $size The aria-setsize for the current iteration. + * @param int $index The aria-posinset for the current iteration. + */ +function wp_print_theme_file_tree( $tree, $level = 2, $size = 1, $index = 1 ) { + global $relative_file, $stylesheet; + + if ( is_array( $tree ) ) { + $index = 0; + $size = count( $tree ); + foreach ( $tree as $label => $theme_file ) : + $index++; + if ( ! is_array( $theme_file ) ) { + wp_print_theme_file_tree( $theme_file, $level, $index, $size ); + continue; + } + ?> +
  • + + +
  • + rawurlencode( $tree ), + 'theme' => rawurlencode( $stylesheet ), + ), + admin_url( 'theme-editor.php' ) + ); + ?> +
  • + + (' . esc_html( $filename ) . ')'; + } + + if ( $relative_file === $filename ) { + echo '' . $file_description . ''; + } else { + echo $file_description; + } + ?> + +
  • + $plugin_file ) : + $index++; + if ( ! is_array( $plugin_file ) ) { + wp_print_plugin_file_tree( $plugin_file, $label, $level, $index, $size ); + continue; + } + ?> +
  • + + +
  • + rawurlencode( $tree ), + 'plugin' => rawurlencode( $plugin ), + ), + admin_url( 'plugin-editor.php' ) + ); + ?> +
  • + + ' . esc_html( $label ) . ''; + } else { + echo esc_html( $label ); + } + ?> + +
  • + = 0; i--) { + var ti = this.treeitems[i]; + if (ti === currentItem) { + break; + } + if (ti.isVisible) { + nextItem = ti; + } + } + + if (nextItem) { + this.setFocusToItem(nextItem); + } + + }; + + TreeLinks.prototype.setFocusToPreviousItem = function (currentItem) { + + var prevItem = false; + + for (var i = 0; i < this.treeitems.length; i++) { + var ti = this.treeitems[i]; + if (ti === currentItem) { + break; + } + if (ti.isVisible) { + prevItem = ti; + } + } + + if (prevItem) { + this.setFocusToItem(prevItem); + } + }; + + TreeLinks.prototype.setFocusToParentItem = function (currentItem) { + + if (currentItem.groupTreeitem) { + this.setFocusToItem(currentItem.groupTreeitem); + } + }; + + TreeLinks.prototype.setFocusToFirstItem = function () { + this.setFocusToItem(this.firstTreeitem); + }; + + TreeLinks.prototype.setFocusToLastItem = function () { + this.setFocusToItem(this.lastTreeitem); + }; + + TreeLinks.prototype.expandTreeitem = function (currentItem) { + + if (currentItem.isExpandable) { + currentItem.domNode.setAttribute('aria-expanded', true); + this.updateVisibleTreeitems(); + } + + }; + + TreeLinks.prototype.expandAllSiblingItems = function (currentItem) { + for (var i = 0; i < this.treeitems.length; i++) { + var ti = this.treeitems[i]; + + if ((ti.groupTreeitem === currentItem.groupTreeitem) && ti.isExpandable) { + this.expandTreeitem(ti); + } + } + + }; + + TreeLinks.prototype.collapseTreeitem = function (currentItem) { + + var groupTreeitem = false; + + if (currentItem.isExpanded()) { + groupTreeitem = currentItem; + } + else { + groupTreeitem = currentItem.groupTreeitem; + } + + if (groupTreeitem) { + groupTreeitem.domNode.setAttribute('aria-expanded', false); + this.updateVisibleTreeitems(); + this.setFocusToItem(groupTreeitem); + } + + }; + + TreeLinks.prototype.updateVisibleTreeitems = function () { + + this.firstTreeitem = this.treeitems[0]; + + for (var i = 0; i < this.treeitems.length; i++) { + var ti = this.treeitems[i]; + + var parent = ti.domNode.parentNode; + + ti.isVisible = true; + + while (parent && (parent !== this.domNode)) { + + if (parent.getAttribute('aria-expanded') == 'false') { + ti.isVisible = false; + } + parent = parent.parentNode; + } + + if (ti.isVisible) { + this.lastTreeitem = ti; + } + } + + }; + + TreeLinks.prototype.setFocusByFirstCharacter = function (currentItem, _char) { + var start, index; + _char = _char.toLowerCase(); + + // Get start index for search based on position of currentItem + start = this.treeitems.indexOf(currentItem) + 1; + if (start === this.treeitems.length) { + start = 0; + } + + // Check remaining slots in the menu + index = this.getIndexFirstChars(start, _char); + + // If not found in remaining slots, check from beginning + if (index === -1) { + index = this.getIndexFirstChars(0, _char); + } + + // If match was found... + if (index > -1) { + this.setFocusToItem(this.treeitems[index]); + } + }; + + TreeLinks.prototype.getIndexFirstChars = function (startIndex, _char) { + for (var i = startIndex; i < this.firstChars.length; i++) { + if (this.treeitems[i].isVisible) { + if (_char === this.firstChars[i]) { + return i; + } + } + } + return -1; + }; + + return TreeLinks; + })(); + + /* jshint ignore:end */ + /* jscs:enable */ + /* eslint-enable */ + return component; })( jQuery ); diff --git a/src/wp-admin/plugin-editor.php b/src/wp-admin/plugin-editor.php index 807cd5e09d..200ebe8f5d 100644 --- a/src/wp-admin/plugin-editor.php +++ b/src/wp-admin/plugin-editor.php @@ -231,7 +231,7 @@ if ( 'POST' === $_SERVER['REQUEST_METHOD'] ) {
    -

    +

    -
    diff --git a/src/wp-admin/theme-editor.php b/src/wp-admin/theme-editor.php index 70629d914d..e275af1fda 100644 --- a/src/wp-admin/theme-editor.php +++ b/src/wp-admin/theme-editor.php @@ -89,6 +89,14 @@ foreach ( $file_types as $type ) { } } +// Move functions.php and style.css to the top. +if ( isset( $allowed_files['functions.php'] ) ) { + $allowed_files = array( 'functions.php' => $allowed_files['functions.php'] ) + $allowed_files; +} +if ( isset( $allowed_files['style.css'] ) ) { + $allowed_files = array( 'style.css' => $allowed_files['style.css'] ) + $allowed_files; +} + if ( empty( $file ) ) { $relative_file = 'style.css'; $file = $allowed_files['style.css']; @@ -205,63 +213,33 @@ foreach ( wp_get_themes( array( 'errors' => null ) ) as $a_stylesheet => $a_them if ( $theme->errors() ) echo '

    ' . __( 'This theme is broken.' ) . ' ' . $theme->errors()->get_error_message() . '

    '; ?> -
    - $absolute_filename ) : - $file_type = substr( $filename, strrpos( $filename, '.' ) ); - - if ( $file_type !== $previous_file_type ) { - if ( '' !== $previous_file_type ) { - echo "\t\n"; - } - - switch ( $file_type ) { - case '.php': - if ( $has_templates || $theme->parent() ) : - echo "\t

    " . __( 'Templates' ) . "

    \n"; - if ( $theme->parent() ) { - echo '

    ' . sprintf( __( 'This child theme inherits templates from a parent theme, %s.' ), - sprintf( '%s', - self_admin_url( 'theme-editor.php?theme=' . urlencode( $theme->get_template() ) ), - $theme->parent()->display( 'Name' ) - ) - ) . "

    \n"; - } - endif; - break; - case '.css': - echo "\t

    " . _x( 'Styles', 'Theme stylesheets in theme editor' ) . "

    \n"; - break; - default: - /* translators: %s: file extension */ - echo "\t

    " . sprintf( __( '%s files' ), $file_type ) . "

    \n"; - break; - } - - echo "\t
      \n"; +
      +

      + parent() ) : + if ( $theme->parent() ) { + /* translators: %s: link to edit parent theme */ + echo '

      ' . sprintf( __( 'This child theme inherits templates from a parent theme, %s.' ), + sprintf( '%s', + self_admin_url( 'theme-editor.php?theme=' . urlencode( $theme->get_template() ) ), + $theme->parent()->display( 'Name' ) + ) + ) . "

      \n"; } - - $file_description = esc_html( get_file_description( $filename ) ); - if ( $filename !== basename( $absolute_filename ) || $file_description !== $filename ) { - $file_description .= '
      (' . esc_html( $filename ) . ')'; - } - - if ( $absolute_filename === $file ) { - $file_description = '' . $file_description . ''; - } - - $previous_file_type = $file_type; -?> -
    • - -
    - + endif; + ?> +
      +
    • +
        + +
      +
    • +
    +

    ' . __('Oops, no such file exists! Double check the name and try again, merci.') . '

    '; else : ?>