├── .nvmrc ├── .gitignore ├── screenshot.png ├── assets └── src │ ├── styles │ ├── components │ │ ├── _feature-requests.scss │ │ ├── _Nav.scss │ │ ├── _sidebar.scss │ │ ├── _footer.scss │ │ ├── _site-content-header.scss │ │ ├── _article.scss │ │ ├── _Pagination.scss │ │ ├── _updates.scss │ │ ├── _comments.scss │ │ ├── _nav-accordion.scss │ │ ├── _PageHistory.scss │ │ └── _header.scss │ ├── base │ │ ├── _fonts.scss │ │ ├── _variables.scss │ │ └── _layout.scss │ ├── editor.scss │ ├── theme.scss │ └── login.scss │ └── scripts │ ├── components │ ├── PageHistory │ │ ├── PageHistorySettings.js │ │ ├── PageHistoryListItem.js │ │ ├── PageHistoryDiff.js │ │ ├── PageHistoryList.js │ │ └── PageHistory.js │ ├── SearchBar │ │ ├── SearchBarSettings.js │ │ ├── SearchBarResult.js │ │ ├── SearchBarResults.js │ │ └── SearchBar.js │ └── NavAccordion │ │ └── NavAccordion.js │ └── theme.js ├── .gitmodules ├── parts ├── pagination.php ├── updates.php ├── article.php └── site-content-heading.php ├── composer.json ├── style.css ├── sidebar.php ├── 404.php ├── .build-script ├── searchform.php ├── .sass-lint.yml ├── inc ├── private-links.php ├── tinyMCE │ └── tinyMCE-typekit.js ├── updates.php ├── editor-mods.php ├── primary-nav.php ├── page-history.php └── search.php ├── template-full-content.php ├── webpack.config.js ├── footer.php ├── package.json ├── index.php ├── comments.php ├── header.php ├── gulpfile.js ├── readme.md └── functions.php /.nvmrc: -------------------------------------------------------------------------------- 1 | v12 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .idea 3 | *.sass-cache/ 4 | node_modules 5 | assets/dist 6 | -------------------------------------------------------------------------------- /screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/humanmade/hm-handbook-theme/HEAD/screenshot.png -------------------------------------------------------------------------------- /assets/src/styles/components/_feature-requests.scss: -------------------------------------------------------------------------------- 1 | .jck-sfr-vote-button--voted { 2 | background: $hm-red; 3 | } 4 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "vendor/hm-pattern-library"] 2 | path = vendor/hm-pattern-library 3 | url = https://github.com/humanmade/hm-pattern-library.git 4 | -------------------------------------------------------------------------------- /parts/pagination.php: -------------------------------------------------------------------------------- 1 | max_num_pages <= 1 ) { 7 | return; 8 | } 9 | 10 | ?> 11 | 12 | 16 | -------------------------------------------------------------------------------- /assets/src/styles/components/_Nav.scss: -------------------------------------------------------------------------------- 1 | .Nav_Item-Private a:before { 2 | $iconSrc: iconSrc( "lock" ); 3 | content: " "; 4 | display: inline-block; 5 | background: url( $iconSrc ) no-repeat center center; 6 | width: 1.125rem; 7 | height: 1.125rem; 8 | margin-right: 3px; 9 | vertical-align: top; 10 | position: relative; 11 | top: -2px; 12 | } 13 | -------------------------------------------------------------------------------- /assets/src/styles/components/_sidebar.scss: -------------------------------------------------------------------------------- 1 | .site-sidebar { 2 | 3 | background-color: var( --hm-light-blue ); 4 | color: var( --hm-dark-grey ); 5 | padding: $base-line-height $gutter-width * 2; 6 | 7 | ul { 8 | margin: 0; 9 | } 10 | 11 | a:hover { 12 | text-decoration: underline; 13 | } 14 | 15 | @media print { 16 | display: none; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "humanmade/hm-handbook-theme", 3 | "version": "0.0.1", 4 | "description": "The theme for the public Human Made handbook.", 5 | "minimum-stability": "stable", 6 | "type": "wordpress-theme", 7 | "config": { 8 | "allow-plugins": { 9 | "composer/installers": true 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /style.css: -------------------------------------------------------------------------------- 1 | /* 2 | Theme Name: HM Handbook Theme 3 | Theme URI: https://handbook.hmn.md/ 4 | Author: Human Made Limited 5 | Author URI: http://hmn.md 6 | Description: Theme for the Human Made employee handbook site. 7 | Version: 1.1.0 8 | License: GNU General Public License v2 or later 9 | License URI: http://www.gnu.org/licenses/gpl-2.0.html 10 | Text Domain: hm-handbook. 11 | -------------------------------------------------------------------------------- /sidebar.php: -------------------------------------------------------------------------------- 1 | 11 | 12 | 30 | -------------------------------------------------------------------------------- /404.php: -------------------------------------------------------------------------------- 1 | 13 | 14 |
15 | 16 |
17 |

18 |
19 | 20 |

21 |
22 |
23 | 24 |
25 | 26 | 27 | -------------------------------------------------------------------------------- /assets/src/styles/components/_footer.scss: -------------------------------------------------------------------------------- 1 | .Footer { 2 | .footer-content { 3 | @include font-body; 4 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif; 5 | 6 | @media #{ $mq-md-up } { 7 | width: calc( 100% - 250px ); // Same as content area 8 | } 9 | 10 | } 11 | 12 | @media print { 13 | display: none; 14 | } 15 | } 16 | 17 | .site-print-footer { 18 | display: none; 19 | 20 | @media print { 21 | display: block; 22 | } 23 | } 24 | 25 | -------------------------------------------------------------------------------- /.build-script: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | if [ -d "$HOME/.nvm" ]; then 4 | export NVM_DIR="$HOME/.nvm" 5 | else 6 | curl -o- https://raw.githubusercontent.com/creationix/nvm/v0.33.1/install.sh | bash 7 | fi 8 | 9 | export NVM_DIR="$HOME/.nvm" 10 | [ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh" 11 | NODE_VERSION=`nvm current` 12 | 13 | # Use the version of node specified in .nvmrc 14 | nvm use || nvm install $(cat .nvmrc) && nvm use 15 | 16 | npm ci 17 | npm run build 18 | 19 | # Restore prior version of Node 20 | nvm use $NODE_VERSION 21 | -------------------------------------------------------------------------------- /assets/src/scripts/components/PageHistory/PageHistorySettings.js: -------------------------------------------------------------------------------- 1 | var strings = ( 'HMHandbookPageHistory' in window ) ? window.HMHandbookPageHistory.strings : {}; 2 | var api_nonce = ( 'HMHandbookPageHistory' in window ) ? window.HMHandbookPageHistory.api_nonce : ''; 3 | var api_base = ( 'HMHandbookPageHistory' in window ) ? window.HMHandbookPageHistory.api_base : ''; 4 | 5 | var defaultStrings = { 6 | listTitle: 'Page History', 7 | loadMore: 'Load more revisions', 8 | }; 9 | 10 | export default { 11 | strings: Object.assign( defaultStrings, strings ), 12 | api_base: api_base, 13 | api_nonce: api_nonce, 14 | } 15 | -------------------------------------------------------------------------------- /parts/updates.php: -------------------------------------------------------------------------------- 1 | 11 | 12 |
13 | 14 |
15 | 16 |

17 | 18 | 19 | 20 |
21 | 22 |
23 | 24 |

25 | 26 | 27 | 28 |
29 | 30 |
31 | -------------------------------------------------------------------------------- /searchform.php: -------------------------------------------------------------------------------- 1 |
2 | 12 |
13 | -------------------------------------------------------------------------------- /assets/src/styles/components/_site-content-header.scss: -------------------------------------------------------------------------------- 1 | .site-content-header { 2 | 3 | margin: $base-line-height auto $base-line-height; 4 | border-bottom: 1px solid $border-color; 5 | padding-bottom: $base-line-height; 6 | 7 | @media #{ $mq-lg-up } { 8 | margin-top: $base-line-height * 2; 9 | margin-bottom: $base-line-height * 2; 10 | } 11 | 12 | &:first-child { 13 | margin-top: 0; 14 | } 15 | 16 | .site-content-header-title { 17 | margin-top: 0; 18 | 19 | &:last-child { 20 | margin-bottom: 0; 21 | } 22 | } 23 | 24 | .site-content-header-title-pre { 25 | @include text-sm; 26 | @include font-heading; 27 | margin-bottom: 0; 28 | } 29 | 30 | } 31 | -------------------------------------------------------------------------------- /assets/src/styles/components/_article.scss: -------------------------------------------------------------------------------- 1 | .article { 2 | 3 | margin: $base-line-height auto $base-line-height; 4 | 5 | @media #{ $mq-lg-up } { 6 | margin-top: $base-line-height * 2; 7 | margin-bottom: $base-line-height * 2; 8 | } 9 | 10 | .article-title { 11 | margin-top: 0; 12 | 13 | &:empty { 14 | display: none; 15 | } 16 | 17 | a:link, 18 | a:visited { 19 | color: inherit; 20 | } 21 | } 22 | 23 | &:first-child { 24 | margin-top: 0; 25 | } 26 | 27 | & + .article { 28 | border-top: 1px solid $border-color; 29 | margin-top: 0; 30 | padding-top: $base-line-height; 31 | 32 | @media #{ $mq-lg-up } { 33 | padding-top: $base-line-height * 2; 34 | } 35 | 36 | } 37 | 38 | } 39 | -------------------------------------------------------------------------------- /assets/src/styles/base/_fonts.scss: -------------------------------------------------------------------------------- 1 | /** 2 | * Prevent FOUC when typekit is loading. 3 | * We need to target each individual element so the 4 | * overall page layout loads, only the text itself pops in once loaded. 5 | */ 6 | h1, 7 | h2, 8 | h3, 9 | h4, 10 | h5, 11 | h6, 12 | p, 13 | ul, 14 | li, 15 | .site-title { 16 | 17 | .wf-loading & { 18 | visibility: hidden; 19 | opacity: 0; 20 | transition: opacity linear 0.05s; 21 | } 22 | 23 | .wf-active & { 24 | visibility: visible; 25 | opacity: 1; 26 | transition: opacity linear 0.05s; 27 | } 28 | 29 | } 30 | 31 | .wf-active { 32 | .NavAccordion ul, 33 | .NavAccordion ol { 34 | transition: opacity linear 0.05s, max-height ease-in-out 0.1s; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /.sass-lint.yml: -------------------------------------------------------------------------------- 1 | options: 2 | merge-default-rules: false 3 | files: 4 | ignore: 5 | - 'dist/**/*' 6 | - 'assets/src/styles/vendor/**/*' 7 | rules: 8 | no-ids: 2 9 | no-important: 2 10 | no-vendor-prefixes: 2 11 | no-url-protocols: 2 12 | indentation: 13 | - 2 14 | - size: tab 15 | single-line-per-selector: 1 16 | one-declaration-per-line: 1 17 | empty-line-between-blocks: 1 18 | brace-style: 1 19 | space-before-brace: 2 20 | space-between-parens: 21 | - 1 22 | - include: true 23 | trailing-semicolon: 2 24 | space-after-colon: 2 25 | space-after-comma: 2 26 | space-around-operator: 2 27 | attribute-quotes: 1 28 | quotes: 29 | - 2 30 | - style: double 31 | no-invalid-hex: 2 32 | -------------------------------------------------------------------------------- /assets/src/scripts/components/SearchBar/SearchBarSettings.js: -------------------------------------------------------------------------------- 1 | var strings = ( 'HMHandbookSearchBarSettings' in window ) ? window.HMHandbookSearchBarSettings.strings : {}; 2 | var api_nonce = ( 'HMHandbookSearchBarSettings' in window ) ? window.HMHandbookSearchBarSettings.api_nonce : ''; 3 | var api_endpoint = ( 'HMHandbookSearchBarSettings' in window ) ? window.HMHandbookSearchBarSettings.api_endpoint : ''; 4 | 5 | var defaultStrings = { 6 | label: 'Search', 7 | button: 'Submit', 8 | placeholder: 'Search the site…', 9 | noResults: 'No results found', 10 | }; 11 | 12 | export default { 13 | strings: Object.assign( defaultStrings, strings ), 14 | api_endpoint: api_endpoint, 15 | api_nonce: api_nonce, 16 | } 17 | -------------------------------------------------------------------------------- /assets/src/styles/editor.scss: -------------------------------------------------------------------------------- 1 | $images-path: "./../../../vendor/hm-pattern-library/assets/images" !default; 2 | 3 | // External 4 | @import "./../../../vendor/hm-pattern-library/assets/sass/juniper.scss"; 5 | 6 | // Base 7 | @import "base/variables"; 8 | @import "base/fonts"; 9 | 10 | .mce-content-body { 11 | width: 90%; 12 | max-width: 40rem; 13 | margin-left: auto; 14 | margin-right: auto; 15 | -webkit-font-smoothing: auto !important; 16 | } 17 | 18 | table { 19 | margin: $base-line-height auto; 20 | } 21 | 22 | .mce-item-table { 23 | border: none; 24 | } 25 | 26 | .mce-item-table td { 27 | @extend td; 28 | border-top: none; 29 | border-left: none; 30 | } 31 | 32 | .mce-item-table th { 33 | @extend td; 34 | border-top: none; 35 | border-left: none; 36 | } 37 | -------------------------------------------------------------------------------- /inc/private-links.php: -------------------------------------------------------------------------------- 1 | $value ) { 15 | $html_attr[] = $key . '="' . esc_attr( $value ) . '"'; 16 | } 17 | 18 | if ( is_user_logged_in() ) { 19 | return sprintf( 20 | '%s', 21 | implode( ' ', $html_attr ), 22 | $content 23 | ); 24 | } 25 | 26 | return sprintf( 27 | '🔒 %s', 28 | wp_login_url( $_SERVER['REQUEST_URI'] ), 29 | $content 30 | ); 31 | } 32 | -------------------------------------------------------------------------------- /template-full-content.php: -------------------------------------------------------------------------------- 1 | 13 | 14 |
15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 |
29 | 30 |
31 | 34 |
35 | 36 | 37 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const webpack = require( 'webpack' ); 2 | 3 | module.exports = { 4 | cache: false, 5 | devtool: 'source-map', 6 | entry: { 7 | theme: [ 8 | './vendor/hm-pattern-library/assets/js/juniper.js', 9 | './assets/src/scripts/theme.js', 10 | ] 11 | }, 12 | output: { 13 | path: 'assets/dist/scripts', 14 | filename: '[name].js', 15 | sourceMapFilename: '[file].map' 16 | }, 17 | module: { 18 | loaders: [ 19 | { 20 | test: /\.jsx?$/, 21 | exclude: /(node_modules|bower_components)/, 22 | loader: "babel-loader", 23 | query: { 24 | presets: [ 'react', 'es2015' ], 25 | } 26 | } 27 | ] 28 | }, 29 | plugins: [ 30 | new webpack.optimize.UglifyJsPlugin({ 31 | compress: { warnings: false }, 32 | sourceMap: true, 33 | }), 34 | ], 35 | externals: { 36 | 'jquery' : 'jQuery' 37 | }, 38 | } 39 | -------------------------------------------------------------------------------- /assets/src/styles/base/_variables.scss: -------------------------------------------------------------------------------- 1 | /** 2 | * Default Colours. 3 | */ 4 | 5 | $hm-handbook-colour-green: #daf2d6; 6 | $border-color-darken: darken( $border-color, 10% ) !default; 7 | 8 | /** 9 | * Variable Mapping 10 | */ 11 | 12 | // Page History 13 | $color-success: $hm-handbook-colour-green !default; 14 | $color-pagehistory-background: $hm_light-grey !default; 15 | $color-pagehistory-list: $hm-medium-grey !default; 16 | 17 | // Sidebar 18 | $color-sidebar-background: $hm-light-grey !default; 19 | 20 | // Login 21 | $login-background-color: $color-primary !default; 22 | $login-logo-image : "#{ $images-path }/logos/logo-white.svg" !default; 23 | $login-form-background : $color-sidebar-background !default; 24 | 25 | $comment_avatar_width: 3rem !default; 26 | 27 | /** 28 | * Breakpoints 29 | */ 30 | $mq-lg-up: "( min-width: 1300px )" !default; 31 | 32 | // Legacy 33 | $white: #FFF; 34 | -------------------------------------------------------------------------------- /assets/src/scripts/components/PageHistory/PageHistoryListItem.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export default class PageHistoryListItem extends React.Component { 4 | 5 | render() { 6 | 7 | var verb = 'create' === this.props.action ? 'created' : 'updated'; 8 | var classNames = [ 'PageHistory_List_Item' ]; 9 | 10 | if ( this.props.active ) { 11 | classNames.push( 'PageHistory_List_Item-Active' ); 12 | } 13 | 14 | return ( 15 |
  • 16 | { e.preventDefault(); this.props.actions.onSelectRevision( this.props ) } }> 17 | { this.props.date } – { this.props.author } 18 | 19 |
  • 20 | ); 21 | } 22 | } 23 | 24 | PageHistoryListItem.defaultProps = { 25 | id: 0, 26 | active: false, 27 | author: '', 28 | action: 'update', 29 | date: '', 30 | content: '', 31 | }; 32 | -------------------------------------------------------------------------------- /assets/src/styles/components/_Pagination.scss: -------------------------------------------------------------------------------- 1 | .Pagination { 2 | 3 | @include clearfix; 4 | 5 | margin: $base-line-height auto; 6 | border-top: 1px solid $border-color; 7 | padding-top: $base-line-height; 8 | text-align: center; 9 | } 10 | 11 | .Pagination-Prev { 12 | float: left; 13 | margin-bottom: 0; 14 | } 15 | 16 | .Pagination-Next { 17 | margin-right: 0; 18 | margin-bottom: 0; 19 | float: right; 20 | } 21 | 22 | .Pagination-Article { 23 | 24 | a { 25 | @extend .btn; 26 | @extend .btn--small; 27 | @extend .btn--tertiary; 28 | margin-right: $gutter-width * 0.25; 29 | } 30 | 31 | .Pagination-Current { 32 | @extend .btn; 33 | @extend .btn--small; 34 | @extend .btn--secondary; 35 | margin-right: $gutter-width * 0.25; 36 | pointer-events: none; 37 | } 38 | 39 | .Pagination-Label { 40 | line-height: 2.25rem; 41 | margin-right: $gutter-width * 0.25; 42 | display: inline-block; 43 | vertical-align: middle; 44 | margin-bottom: .75rem; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /footer.php: -------------------------------------------------------------------------------- 1 | 18 | 19 | 22 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /assets/src/styles/components/_updates.scss: -------------------------------------------------------------------------------- 1 | .updates { 2 | 3 | display: flex; 4 | flex-wrap: wrap; 5 | 6 | &--heading { 7 | flex: 1 0 100%; 8 | margin: 0 0 #{ $gutter-width * 0.5 }; 9 | } 10 | 11 | &--box { 12 | background-color: $hm-light-grey; 13 | padding: $gutter-width; 14 | margin: 0 #{ $gutter-width * 0.5 } $gutter-width 0; 15 | flex: 1 0 100%; 16 | 17 | @media ( min-width: 1400px ) { 18 | flex-basis: calc( 50% - #{ $gutter-width } ); 19 | } 20 | } 21 | 22 | &--list { 23 | margin: 0; 24 | padding: 0; 25 | } 26 | 27 | &--list-item { 28 | list-style: none; 29 | border-bottom: 1px solid darken( $hm-light-grey, 10% ); 30 | padding: #{ $gutter-width * 0.333 } 0; 31 | 32 | &:last-child { 33 | padding-bottom: 0; 34 | border-bottom: 0; 35 | } 36 | } 37 | 38 | &--link { 39 | display: block; 40 | line-height: 1.2; 41 | } 42 | 43 | &--link-meta { 44 | display: block; 45 | padding-top: 0.25em; 46 | font-size: 70%; 47 | line-height: 1.2; 48 | } 49 | 50 | } 51 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "HM_Handbook", 3 | "version": "1.0.0", 4 | "description": "Human Made Handbook Theme", 5 | "main": "index.php", 6 | "author": "matth_eu", 7 | "license": "GPL-2.0", 8 | "private": true, 9 | "devDependencies": { 10 | "autoprefixer": "^6.3.4", 11 | "babel-core": "^6.10.4", 12 | "babel-loader": "^6.2.4", 13 | "babel-preset-es2015": "^6.9.0", 14 | "babel-preset-react": "^6.11.1", 15 | "classnames": "^2.2.5", 16 | "diff-match-patch": "^1.0.0", 17 | "exports-loader": "^0.6.3", 18 | "fibers": "^5.0.0", 19 | "gulp": "^4.0.2", 20 | "gulp-cli": "^2.3.0", 21 | "gulp-postcss": "^9.0.1", 22 | "gulp-sass": "^5.1.0", 23 | "gulp-sass-lint": "^1.4.0", 24 | "gulp-sourcemaps": "^3.0.0", 25 | "gulp-watch": "^5.0.1", 26 | "postcss": "^8.3.1", 27 | "react": "^15.2.1", 28 | "react-dom": "^15.2.1", 29 | "sass": "^1.49.11", 30 | "webpack": "^1.13.1", 31 | "whatwg-fetch": "^1.0.0" 32 | }, 33 | "scripts": { 34 | "build": "gulp" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /assets/src/scripts/components/SearchBar/SearchBarResult.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import classNames from 'classnames'; 3 | 4 | /** 5 | * History List. 6 | */ 7 | export default class SearchBarResult extends Component { 8 | 9 | render() { 10 | 11 | let classes = classNames({ 12 | 'search-bar__result': true, 13 | 'search-bar__result--comment': 'comment' === this.props.type, 14 | 'search-bar__result--post': 'post' === this.props.type 15 | }); 16 | 17 | let style = { width: ( this.props.containerWidth - 60 ) + 'px' } 18 | 19 | return ( 20 |
    21 | 22 |

    { this.props.title }

    23 |
    { this.props.excerpt }
    24 |
    25 |
    26 | ); 27 | } 28 | 29 | } 30 | 31 | SearchBarResult.propTypes = { 32 | query: React.PropTypes.string, 33 | containerWidth: React.PropTypes.number, 34 | }; 35 | 36 | SearchBarResult.defaultProps = { 37 | query: '', 38 | containerWidth: 0, 39 | } 40 | -------------------------------------------------------------------------------- /parts/article.php: -------------------------------------------------------------------------------- 1 | 13 | 14 |
    > 15 | 16 | 17 | 18 | 19 | 20 |

    21 | 22 |

    23 | 24 | 25 | 26 |

    27 | 28 | 29 | 30 |

    31 | 32 | 33 | 34 | 35 | 36 |
    37 | 38 |
    39 | 40 | 41 | 51 | 52 | 53 | 54 |
    55 | -------------------------------------------------------------------------------- /assets/src/scripts/components/PageHistory/PageHistoryDiff.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import DiffMatchPatch from 'diff-match-patch'; 3 | 4 | /** 5 | * Diff component. 6 | */ 7 | export default class PageHistoryDiff extends React.Component { 8 | 9 | render() { 10 | return
    11 | { this.getDiff().map( ( part, i ) => { 12 | if ( 1 === part[0] ) { 13 | return { part[1] } 14 | } else if ( -1 === part[0] ) { 15 | return { part[1] } 16 | } else { 17 | return { part[1] } 18 | } 19 | } ) } 20 |
    21 | } 22 | 23 | getDiff() { 24 | 25 | if ( ! ( this.props.diff_a && this.props.diff_b ) ) { 26 | return []; 27 | } 28 | 29 | var dmp = new DiffMatchPatch(); 30 | 31 | var diff = dmp.diff_main( 32 | this.props.diff_b.replace( /[\n|\r]{2,}/g, "\n\n" ), 33 | this.props.diff_a.replace( /[\n|\r]{2,}/g, "\n\n" ) 34 | ); 35 | 36 | dmp.diff_cleanupSemantic( diff ); 37 | 38 | return diff; 39 | } 40 | }; 41 | 42 | PageHistoryDiff.propTypes = { 43 | diff_a: React.PropTypes.string, 44 | diff_b: React.PropTypes.string, 45 | }; 46 | 47 | PageHistoryDiff.defaultProps = { 48 | diff_a: '', 49 | diff_b: '', 50 | }; 51 | -------------------------------------------------------------------------------- /index.php: -------------------------------------------------------------------------------- 1 | 20 | 21 |
    22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 |
    44 | 45 |
    46 | 49 |
    50 | 51 | 52 | -------------------------------------------------------------------------------- /assets/src/scripts/theme.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import PageHistory from './components/PageHistory/PageHistory.js'; 4 | import SearchBar from './components/SearchBar/SearchBar.js'; 5 | import initNavAccordion from './components/NavAccordion/NavAccordion.js'; 6 | 7 | if ( 'HMHandbookPageHistory' in window ) { 8 | 9 | var revisions, containers; 10 | 11 | containers = document.querySelectorAll( 'body.single-post .site-content .article, body.page .site-content .article' ); 12 | 13 | for ( var i = 0; i < containers.length; i++ ) { 14 | let el = document.createElement( 'DIV' ); 15 | let id = parseInt( window.HMHandbookPageHistory.post_id, 10 ); 16 | containers[ i ].appendChild( el ); 17 | ReactDOM.render( , el ); 18 | } 19 | 20 | } 21 | 22 | var searchBarContainers = document.querySelectorAll( '.search-container' ); 23 | 24 | searchBarContainers.forEach( searchBarContainer => { 25 | while ( searchBarContainer.firstChild ) { 26 | searchBarContainer.removeChild( searchBarContainer.firstChild ); 27 | } 28 | 29 | ReactDOM.render( 30 | , 31 | searchBarContainer 32 | ); 33 | } ); 34 | 35 | // Init Accordion Nav for all NavAccordion Items. 36 | Array.prototype.forEach.call( 37 | document.getElementsByClassName( 'NavAccordion_Item' ), 38 | initNavAccordion 39 | ); 40 | 41 | -------------------------------------------------------------------------------- /comments.php: -------------------------------------------------------------------------------- 1 | 23 | 24 |
    25 | 26 | 27 | 28 |

    29 | 33 |

    34 | 35 |
      36 | 'ol', 39 | 'short_ping' => true, 40 | 'avatar_size' => 54, 41 | ) ); 42 | ?> 43 |
    44 | 45 | 46 | 47 | 48 |

    49 | 50 | 51 | 52 | 53 |
    54 | -------------------------------------------------------------------------------- /header.php: -------------------------------------------------------------------------------- 1 | section 6 | * 7 | * @link https://developer.wordpress.org/themes/basics/template-files/#template-partials 8 | * 9 | * @package hm-handbook 10 | */ 11 | 12 | namespace HM_Handbook; 13 | 14 | ?> 15 | > 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | > 28 | 29 | 52 | 53 |
    54 | 55 | 56 | 57 |
    58 | -------------------------------------------------------------------------------- /inc/tinyMCE/tinyMCE-typekit.js: -------------------------------------------------------------------------------- 1 | /* global tinymce */ 2 | tinymce.PluginManager.add( 'typekit', function( editor ) { 3 | 4 | function addScriptToHead() { 5 | // Get the DOM document object for the IFRAME 6 | var doc = editor.getDoc(); 7 | 8 | // Create the script we will add to the header asynchronously 9 | var jscript = "(function() {\n\ 10 | var config = {\n\ 11 | kitId: 'mwe8dvt'\n\ 12 | };\n\ 13 | var d = false;\n\ 14 | var tk = document.createElement('script');\n\ 15 | tk.src = '//use.typekit.net/' + config.kitId + '.js';\n\ 16 | tk.type = 'text/javascript';\n\ 17 | tk.async = 'true';\n\ 18 | tk.onload = tk.onreadystatechange = function() {\n\ 19 | var rs = this.readyState;\n\ 20 | if (d || rs && rs != 'complete' && rs != 'loaded') return;\n\ 21 | d = true;\n\ 22 | try { Typekit.load(config); } catch (e) {}\n\ 23 | };\n\ 24 | var s = document.getElementsByTagName('script')[0];\n\ 25 | s.parentNode.insertBefore(tk, s);\n\ 26 | })();"; 27 | 28 | // Create a script element and insert the TypeKit code into it 29 | var script = doc.createElement( 'script' ); 30 | script.type = 'text/javascript'; 31 | script.appendChild( doc.createTextNode( jscript ) ); 32 | 33 | // Add the script to the header 34 | doc.getElementsByTagName( 'head' )[0].appendChild( script ); 35 | } 36 | 37 | // Support both TinyMCE 3 and 4. 38 | if ( 3 < parseInt( tinymce.majorVersion ) ) { 39 | editor.on( 'preInit', function() { 40 | addScriptToHead(); 41 | }); 42 | } else { 43 | editor.onPreInit.add( function( editor ) { 44 | addScriptToHead(); 45 | }); 46 | } 47 | }); 48 | -------------------------------------------------------------------------------- /assets/src/styles/base/_layout.scss: -------------------------------------------------------------------------------- 1 | html { 2 | min-height: 100%; 3 | height: 100%; 4 | } 5 | 6 | body { 7 | height: 100%; 8 | } 9 | 10 | .site-container { 11 | @media #{ $mq-md-up } { 12 | display: flex; 13 | flex-wrap: wrap; 14 | align-items: stretch; 15 | min-height: 100%; 16 | min-height: calc( 100% - 6rem ); // 6rem = height of header. 17 | } 18 | } 19 | 20 | .site-sidebar { 21 | 22 | padding: $margin-vertical-sm $gutter-width * 2; 23 | 24 | @media #{ $mq-md-up } { 25 | width: 300px; 26 | } 27 | } 28 | 29 | .site-content-container { 30 | 31 | width: 100%; 32 | padding: $margin-vertical-sm $gutter-width * 2; 33 | 34 | @media #{ $mq-md-up } { 35 | width: calc( 100% - 300px ); 36 | max-width: calc( 40rem + #{ $gutter-width + 250px + $gutter-width * 2 } ); 37 | } 38 | 39 | @media #{ $mq-lg-up } { 40 | max-width: calc( 40rem + #{ $gutter-width * 2 + 250px + $gutter-width * 4 } ); 41 | padding: $margin-vertical-sm * 2 ( $gutter-width * 4 ); 42 | } 43 | } 44 | 45 | .site-content { 46 | 47 | width: 100%; 48 | float: left; 49 | 50 | @media #{ $mq-md-up } { 51 | width: calc( 100% - 250px ); 52 | padding-right: $gutter-width; 53 | } 54 | 55 | @media #{ $mq-lg-up } { 56 | padding-right: $gutter-width * 2; 57 | } 58 | 59 | .page-template-template-full-content &, 60 | .cpt_feature_requests-template & { 61 | width: 100%; 62 | padding-right: 0; 63 | } 64 | } 65 | 66 | .site-content-sidebar { 67 | 68 | width: 100%; 69 | float: left; 70 | 71 | @media #{ $mq-md-up } { 72 | width: 250px; 73 | } 74 | 75 | } 76 | 77 | .site-footer { 78 | width: 100%; 79 | clear: both; 80 | padding-top: 0; 81 | } 82 | -------------------------------------------------------------------------------- /assets/src/scripts/components/SearchBar/SearchBarResults.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import SearchBarResult from './SearchBarResult.js'; 3 | import SearchBarSettings from './SearchBarSettings.js'; 4 | 5 | /** 6 | * History List. 7 | */ 8 | export default class SearchBarResults extends Component { 9 | 10 | render() { 11 | 12 | // Get the height of the whole document. 13 | // Bit convoluted, taken from jQuery. 14 | var height = Math.max( 15 | document.body.scrollHeight, 16 | document.body.offsetHeight, 17 | document.documentElement.clientHeight, 18 | document.documentElement.scrollHeight, 19 | document.documentElement.offsetHeight 20 | ); 21 | 22 | var adminBar = document.getElementById( 'wpadminbar' ); 23 | 24 | if ( adminBar ) { 25 | height = height - adminBar.offsetHeight; 26 | } 27 | 28 | let displayNoResults = this.props.query.length > 1 && this.props.results.length < 1; 29 | 30 | return ( 31 |
    32 | 33 |

    36 | { SearchBarSettings.strings.noResults } 37 |

    38 | 39 | { this.props.results.map( ( result, i ) => { 40 | return 45 | })} 46 | 47 |
    48 | ); 49 | } 50 | 51 | } 52 | 53 | SearchBarResults.propTypes = { 54 | results: React.PropTypes.array, 55 | containerWidth: React.PropTypes.number, 56 | query: React.PropTypes.string, 57 | }; 58 | 59 | SearchBarResults.defaultProps = { 60 | results: [], 61 | containerWidth: 0, 62 | query: '', 63 | } 64 | -------------------------------------------------------------------------------- /inc/updates.php: -------------------------------------------------------------------------------- 1 | 'page' ]; 13 | 14 | if ( 'edits' === $latest ) { 15 | $args = array_merge( $args, [ 'orderby' => 'modified', 'suppress_filters' => false ] ); 16 | 17 | add_filter( 'posts_where', __NAMESPACE__ . '\\get_edited_pages_only' ); 18 | } 19 | 20 | $posts = get_posts( $args ); 21 | 22 | remove_filter( 'posts_where', __NAMESPACE__ . '\\get_edited_pages_only' ); 23 | 24 | $output = '
      '; 25 | foreach ( $posts as $post ) { 26 | $output .= '
    1. '; 27 | $output .= '' . $post->post_title . ''; 28 | $output .= ''; 29 | 30 | if ( 'posts' === $latest ) { 31 | $output .= sprintf( 32 | esc_html__( 'Posted in %1$s on %2$s', 'hm-handbook' ), 33 | get_the_title( $post->post_parent ), 34 | date_i18n( get_option( 'date_format' ), strtotime( $post->post_date ) ) 35 | ); 36 | } else if ( 'edits' === $latest ) { 37 | $output .= sprintf( 38 | esc_html__( 'Last updated on %s', 'hm-handbook' ), 39 | date_i18n( get_option( 'date_format' ), strtotime( $post->post_modified ) ) 40 | ); 41 | } 42 | 43 | $output .= ''; 44 | $output .= '
    2. '; 45 | } 46 | $output .= '
    '; 47 | 48 | echo $output; 49 | } 50 | 51 | /** 52 | * Modify SQL query to only return edited pages 53 | * 54 | * @param string $where Existing SQL query string 55 | * @return string $where New SQL query string 56 | */ 57 | function get_edited_pages_only( $where = '' ) { 58 | global $wpdb; 59 | 60 | $where .= " AND $wpdb->posts.post_date != $wpdb->posts.post_modified"; 61 | 62 | return $where; 63 | } 64 | -------------------------------------------------------------------------------- /inc/editor-mods.php: -------------------------------------------------------------------------------- 1 | 2 | 3 |
    4 |

    Search results

    5 |

    6 |
    7 | 8 | 9 | 10 |
    11 | 12 |

    13 | taxonomy ); 16 | printf( __( '%s Archive', 'hm-handbook' ), esc_html( $tax->labels->singular_name ) ); 17 | ?> 18 |

    19 | 20 |

    21 | 22 | 25 |
    26 | 27 |
    28 | 29 | 30 |
    31 | 32 | 33 | 34 |
    35 | 36 |

    37 | 38 |

    39 | 40 | 43 |
    44 | 45 |
    46 | 47 | 48 |
    49 | 50 | 51 | 52 |
    53 | 54 |

    55 | 64 |

    65 | 66 |

    67 | 78 |

    79 | 80 |
    81 | 82 | 83 | -------------------------------------------------------------------------------- /assets/src/scripts/components/NavAccordion/NavAccordion.js: -------------------------------------------------------------------------------- 1 | export default function( navAccordionItem ) { 2 | 3 | var subNav = Array.prototype.filter.call( navAccordionItem.children, function( el ) { 4 | return el.tagName === 'UL'; 5 | } ); 6 | 7 | if ( subNav.length < 1 ) { 8 | return; 9 | } else { 10 | subNav = subNav[0]; 11 | } 12 | 13 | var navAccordionToggle = document.createElement( 'BUTTON' ); 14 | 15 | var span = document.createElement('SPAN'); 16 | span.appendChild( document.createTextNode( 'expand child menu' ) ); 17 | span.classList.add( 'screen-reader-text' ); 18 | 19 | navAccordionToggle.appendChild( span ) ; 20 | navAccordionToggle.classList.add( 'Btn' ); 21 | navAccordionToggle.classList.add( 'NavAccordion_Toggle' ); 22 | navAccordionToggle.setAttribute( 'role', 'button' ); 23 | navAccordionToggle.setAttribute( 'aria-haspopup', 'true' ); 24 | 25 | var anchor = Array.prototype.filter.call( navAccordionItem.children, function( el ) { 26 | return el.tagName === 'A'; 27 | } ); 28 | 29 | if ( anchor.length ) { 30 | anchor[0].parentNode.insertBefore( navAccordionToggle, subNav ); 31 | } 32 | 33 | var toggleSubNav = function( show ) { 34 | 35 | if ( 'undefined' === typeof show ) { 36 | show = navAccordionItem.classList.contains( 'NavAccordion_Item-Closed' ); 37 | navAccordionToggle.setAttribute( 'aria-expanded', 'false' ); 38 | } 39 | 40 | if ( show ) { 41 | navAccordionItem.classList.remove( 'NavAccordion_Item-Closed' ); 42 | navAccordionItem.classList.add( 'NavAccordion_Item-Open' ); 43 | navAccordionToggle.classList.add( 'NavAccordion_Toggle-Open' ); 44 | navAccordionToggle.setAttribute( 'aria-expanded', 'true' ); 45 | subNav.style.display = 'block'; 46 | } else { 47 | navAccordionItem.classList.add( 'NavAccordion_Item-Closed' ); 48 | navAccordionItem.classList.remove( 'NavAccordion_Item-Open' ); 49 | navAccordionToggle.classList.remove( 'NavAccordion_Toggle-Open' ); 50 | navAccordionToggle.setAttribute( 'aria-expanded', 'false' ); 51 | subNav.style.display = 'none'; 52 | } 53 | 54 | } 55 | 56 | if ( navAccordionItem.classList.contains( 'NavAccordion_Item-Active' ) ) { 57 | toggleSubNav( true ); 58 | } else if ( subNav.getElementsByClassName( 'NavAccordion_Item-Active' ).length > 0 ) { 59 | toggleSubNav( true ); 60 | } else { 61 | toggleSubNav( false ); 62 | } 63 | 64 | navAccordionToggle.addEventListener( 'click', function( event ) { 65 | event.preventDefault(); 66 | navAccordionToggle.blur(); 67 | toggleSubNav(); 68 | }); 69 | } 70 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | const gulp = require( 'gulp' ); 2 | const webpack = require( 'webpack' ); 3 | const sourcemaps = require( 'gulp-sourcemaps' ); 4 | const postcss = require( 'gulp-postcss' ); 5 | const autoprefixer = require( 'autoprefixer' ); 6 | const sass = require( 'gulp-sass' )( require( 'sass' ) ); 7 | const sassLint = require( 'gulp-sass-lint' ); 8 | const Fiber = require( 'fibers' ); 9 | 10 | // All the configs for different tasks. 11 | const config = { 12 | sass: { 13 | outputStyle: 'compressed' 14 | }, 15 | webpack: require( './webpack.config.js' ), 16 | imagemin: { 17 | progressive: true, 18 | svgoPlugins: [ 19 | { removeViewBox: false }, 20 | { cleanupIDs: false } 21 | ], 22 | }, 23 | postcss: [ 24 | autoprefixer( { browsers: ['last 3 versions'] } ), 25 | ], 26 | compiler: require('sass'), 27 | fiber: Fiber, 28 | }; 29 | 30 | // Compile and minify CSS. 31 | function styles() { 32 | return gulp.src('./assets/src/styles/*.scss') 33 | .pipe(sourcemaps.init()) 34 | .pipe(sass(config.sass).on('error', sass.logError)) 35 | .pipe(postcss(config.postcss)) 36 | .pipe(sourcemaps.write('.')) 37 | .pipe(gulp.dest('./assets/dist/styles')); 38 | } 39 | 40 | // Bundle JS. 41 | function js( callback ) { 42 | // Set production environment to ensure webpack dev version isn't used. 43 | // Change to dev during developemnt to get more useful errors. 44 | config.webpack.plugins.push( new webpack.DefinePlugin({ 45 | "process.env": { 46 | NODE_ENV: JSON.stringify( "production" ), 47 | } 48 | })); 49 | 50 | webpack( 51 | config.webpack, 52 | function( err, stats ) { 53 | if ( stats.compilation.errors.length > 0 ) { 54 | console.log(stats.compilation.errors.toString()); 55 | } 56 | if ( stats.compilation.warnings.length > 0 ) { 57 | console.log(stats.compilation.warnings.toString() ); 58 | } 59 | callback(); 60 | } 61 | ); 62 | } 63 | 64 | function lintSass() { 65 | return gulp.src( [ './assets/src/styles/**/*.s+(a|c)ss', '!./assets/src/styles/editor.scss', '!./assets/src/styles/login.scss' ] ) 66 | .pipe( sassLint( { configFile: '.sass-lint.yml' } ) ) 67 | .pipe( sassLint.format() ) 68 | .pipe( sassLint.failOnError() ) 69 | } 70 | 71 | // Watch for changes in JS/CSS. 72 | function watch() { 73 | gulp.watch('assets/src/styles/**/*.scss', ['styles', 'lint-sass']); 74 | gulp.watch(['assets/src/scripts/**/*.js', 'assets/src/scripts/**/*.jsx'], ['js']); 75 | } 76 | 77 | module.exports = { 78 | styles, 79 | js, 80 | lintSass, 81 | watch, 82 | default: gulp.parallel(styles, js, lintSass), 83 | } 84 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | ## Development Guidelines 2 | 3 | 4 | * Theme styles use SCSS and are stored in `assets/src/styles`. 5 | * Theme JavaScript files are in `assets/src/scripts`. 6 | * Theme Images files are in `assets/src/images`. 7 | 8 | The theme uses [gulp](http://gulpjs.com) to run various tasks such as compiling CSS, bundling JS and minifiying images. 9 | 10 | * Compiled/proccessed files are not tracked in git and MUST be built locally and on deploy. 11 | * All compiled/proccessed assets are be kept in `assets/dist`. 12 | * Always load assets from `assets/dist`, you probably shouldn't be loading anything from `assets/src`. 13 | 14 | ### Releasing a new version. 15 | 16 | It is essential that whenever a new version is released, we increment the version number in the theme stylesheet ([`style.css`](https://github.com/humanmade/hm-handbook-theme/blob/main/style.css)). This is used to version all theme assets, and bumping this is necessary to ensure nobody gets cached files. This also includes the cached asset integrity hash used by the Altis browser security module, and without the change, assets may fail to load. 17 | 18 | ### Dev Setup 19 | 20 | **Dependencies** 21 | 22 | * A development environment and working WordPress install. e.g. [Salty Wordpress](https://github.com/humanmade/Salty-WordPress) 23 | * [Git](https://git-scm.com) 24 | * [NPM](http://blog.npmjs.org/post/85484771375/how-to-install-npm) 25 | * [Gulp CLI](http://gulpjs.com/) 26 | 27 | **Getting it set up** 28 | 29 | 1. Clone the repository to the `themes` directory of your WordPress install. `git clone --recursive git@github.com:humanmade/hm-handbook-theme.git` 30 | 1. If you didn't pass `--recursive` when cloning in the previous step, you need to make sure you run `git submodule update --recursive --init` from inside the hm-handbook-theme directory to pull down the submodules that are used in this theme. 31 | 1. `npm install` to install all required dependencies. 32 | 1. `gulp` to run all the tasks required to build the theme. 33 | 1. `gulp watch` to watch for changes, and run required tasks automatically. 34 | 35 | ### Updating HM Pattern Library 36 | 37 | The pattern library is a submodule checked out to `/vendor/hm-pattern-library`. We are using the compiled version of this so you should check out either a tagged release or the `gh-pages` branch when updating. 38 | 39 | Example of what needs to be done: 40 | 41 | 1. `cd vendor/hm-pattern-library` 42 | 1. `git fetch --tags && git checkout 1.0` or `git checkout gh-pages && git pull` 43 | 1. `cd ../../ && git add vendor/hm-pattern-library && commit -m 'Update HM Pattern Library'` 44 | -------------------------------------------------------------------------------- /assets/src/styles/components/_comments.scss: -------------------------------------------------------------------------------- 1 | .comment-list { 2 | list-style: none; 3 | margin: 0; 4 | padding: 0; 5 | } 6 | 7 | .comment-list .comment { 8 | 9 | margin: 0; 10 | padding: $base-line-height auto $base-line-height; 11 | 12 | @media #{ $mq-lg-up } { 13 | padding-top: $base-line-height * 2; 14 | padding-bottom: $base-line-height * 2; 15 | } 16 | 17 | & + .comment { 18 | border-top: 1px solid $border-color; 19 | margin-top: 0; 20 | padding-top: $base-line-height; 21 | 22 | @media #{ $mq-lg-up } { 23 | padding-top: $base-line-height * 2; 24 | } 25 | 26 | } 27 | 28 | .children { 29 | list-style: none; 30 | margin: 0 0 0 $gutter-width; 31 | padding: 0; 32 | 33 | .comment { 34 | padding-left: $gutter-width; 35 | } 36 | 37 | } 38 | } 39 | 40 | 41 | .comment-meta { 42 | 43 | margin-bottom: $base-line-height; 44 | padding-left: calc( #{ $comment_avatar_width } + #{ $gutter-width } ); 45 | 46 | .avatar { 47 | width: $comment_avatar_width; 48 | height: $comment_avatar_width; 49 | float: left; 50 | margin-left: calc( #{ -1 * $comment_avatar_width } - #{ $gutter-width } ); 51 | border-radius: 2px; 52 | } 53 | 54 | .comment-author { 55 | @include font-heading; 56 | color: $hm-dark-grey; 57 | 58 | a { 59 | color: inherit; 60 | } 61 | } 62 | 63 | .comment-metadata { 64 | @include text-sm; 65 | 66 | a { 67 | color: inherit; 68 | } 69 | } 70 | 71 | .edit-link { 72 | display: none; 73 | } 74 | } 75 | 76 | .comment-reply-link { 77 | @extend .btn; 78 | @extend .btn--small; 79 | @extend .btn--tertiary; 80 | } 81 | 82 | .comment-reply-title a { 83 | @extend .btn; 84 | @extend .btn--small; 85 | float: right; 86 | } 87 | 88 | .comment-awaiting-moderation { 89 | background: $hm-light-grey; 90 | border-radius: 2px; 91 | padding: $base-line-height * 0.5 $gutter-width; 92 | margin-left: calc( ( -1 * $comment_avatar_width ) - #{ $gutter-width } ); 93 | clear: both; 94 | } 95 | 96 | .comment-form { 97 | 98 | .logged-in-as { 99 | 100 | background: $hm-light-grey; 101 | border-radius: 2px; 102 | padding: $base-line-height * 0.5 $gutter-width; 103 | 104 | a { 105 | color: inherit; 106 | } 107 | } 108 | 109 | label { 110 | @include font-heading; 111 | color: $hm-dark-grey; 112 | @include text-sm; 113 | margin-top: $base-line-height * 0.5; 114 | margin-bottom: 0; 115 | } 116 | 117 | .submit { 118 | @extend .btn; 119 | @extend .btn--secondary; 120 | } 121 | 122 | input[type="email"], 123 | input[type="url"] { 124 | @extend .form__field; 125 | font-family: $font-family-code; 126 | 127 | &::placeholder { 128 | font-style: normal; 129 | } 130 | } 131 | 132 | } 133 | -------------------------------------------------------------------------------- /assets/src/scripts/components/SearchBar/SearchBar.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import SearchBarResults from './SearchBarResults.js'; 3 | import classNames from 'classnames'; 4 | import 'whatwg-fetch'; 5 | import SearchBarSettings from './SearchBarSettings.js'; 6 | 7 | export default class SearchBar extends Component { 8 | 9 | constructor( props ) { 10 | super( props ); 11 | this.state = { 12 | query: this.props.query, 13 | results: [], 14 | focused: false, 15 | loading: false, 16 | }; 17 | } 18 | 19 | render() { 20 | 21 | let searchBarClassNames = classNames({ 22 | 'search-bar': true, 23 | 'search-bar--focused': this.state.focused, 24 | 'search-bar--has-results': this.state.results.length, 25 | 'search-bar--loading': this.state.loading, 26 | }); 27 | 28 | return
    29 | 30 |
    { this.onSearch() } } 36 | > 37 | 38 | 39 | 40 | this.onSearch() } 50 | onFocus={ () => this.onFocus() } 51 | onBlur={ () => this.onBlur() } 52 | /> 53 | 54 | 58 | 59 |
    60 | 61 | 66 | 67 |
    68 | } 69 | 70 | onSearch( e ) { 71 | 72 | var query = this.refs.input.value; 73 | 74 | if ( query.length < 2 ) { 75 | this.setState( { query: query, results: [], loading: false } ) 76 | return; 77 | } 78 | 79 | this.setState( { query: query, loading: true } ); 80 | this.fetchResults( query ) 81 | } 82 | 83 | fetchResults( query ) { 84 | let api_endpoint = SearchBarSettings.api_endpoint; 85 | let api_nonce = SearchBarSettings.api_nonce; 86 | 87 | api_endpoint += `?query=${ encodeURIComponent( query ) }`; 88 | 89 | fetch( api_endpoint, { 90 | credentials: 'include', 91 | headers: new Headers({ 92 | 'X-WP-Nonce': api_nonce 93 | }), 94 | } ).then( ( response ) => { 95 | if ( response.ok ) { 96 | return response.json(); 97 | } 98 | }).then( ( json ) => { 99 | this.setState( { results: json.results } ); 100 | 101 | // Delay disable loading just a little. 102 | window.setTimeout( () => { 103 | this.setState( { loading: false } ); 104 | }, 500 ); 105 | 106 | }); 107 | } 108 | 109 | onFocus() { 110 | this.setState( { focused: true } ); 111 | } 112 | 113 | onBlur() { 114 | window.setTimeout( () => { 115 | this.setState( { focused: false } ); 116 | }, 500 ); 117 | } 118 | 119 | } 120 | 121 | SearchBar.propTypes = { 122 | query: React.PropTypes.string, 123 | containerEl: React.PropTypes.object, // DOM element. 124 | }; 125 | 126 | SearchBar.defaultProps = { 127 | containerEl: null, 128 | }; 129 | -------------------------------------------------------------------------------- /assets/src/scripts/components/PageHistory/PageHistoryList.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PageHistoryListItem from './PageHistoryListItem.js'; 3 | import PageHistorySettings from './PageHistorySettings.js'; 4 | 5 | /** 6 | * History List. 7 | */ 8 | export default class PageHistoryList extends React.Component { 9 | 10 | constructor( props ) { 11 | super( props ); 12 | this.state = { 13 | expanded: false, 14 | }; 15 | } 16 | 17 | render() { 18 | 19 | var actions, containerClasses, toggleButtonClasses, loadMorebuttonClasses, toggleExpandedState; 20 | 21 | actions = { 22 | onSelectRevision: this.props.actions.onSelectRevision, 23 | onfetchRevisions: this.props.actions.onFetchRevisions, 24 | }; 25 | 26 | containerClasses = [ 'PageHistory_List_Container' ]; 27 | containerClasses.push( this.state.expanded ? 'PageHistory_List_Container-Expanded' : 'PageHistory_List_Container-Collapsed' ); 28 | 29 | toggleButtonClasses = [ 'btn btn--small btn--toggle' ]; 30 | toggleButtonClasses.push( this.state.expanded ? ' btn--state-expanded' : null ); 31 | 32 | loadMorebuttonClasses = [ 'btn btn--small btn-Link' ]; 33 | loadMorebuttonClasses.push( this.props.loading ? 'btn-Loading' : null ); 34 | 35 | return ( 36 |
    37 |
    38 | 39 | 40 | 41 |

    { PageHistorySettings.strings.listTitle }

    42 |
      43 | { this.props.revisions.map( revision => { 44 | 45 | if ( ! 'active' in revision ) { 46 | revision.active = false; 47 | } 48 | 49 | return ; 50 | 51 | })} 52 |
    53 | 57 |
    58 | 59 | 78 |
    79 | ); 80 | } 81 | 82 | onPrint() { 83 | // Load. 84 | this.props.actions.onFetchRevisions().then( () => { 85 | // Expand. 86 | this.setState( { expanded: true } ); 87 | 88 | // Print. 89 | window.print(); 90 | } ); 91 | } 92 | 93 | onToggleExpanded() { 94 | 95 | var newState = ! this.state.expanded; 96 | 97 | this.setState( { 98 | expanded: newState 99 | } ); 100 | 101 | // Load revisions if have more and there are none. 102 | // Used on first expansion of component. 103 | if ( this.props.revisions.length < 1 && this.props.hasMore ) { 104 | this.props.actions.onFetchRevisions(); 105 | } 106 | 107 | if ( ! newState ) { 108 | this.props.actions.onClearDiff(); 109 | } 110 | } 111 | } 112 | 113 | PageHistoryList.propTypes = { 114 | revisions: React.PropTypes.array, 115 | loading: React.PropTypes.bool, 116 | hasMore: React.PropTypes.bool, 117 | }; 118 | 119 | PageHistoryList.defaultProps = { 120 | revisions: [], 121 | loading: false, 122 | hasMore: true, 123 | } 124 | -------------------------------------------------------------------------------- /assets/src/scripts/components/PageHistory/PageHistory.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PageHistoryDiff from './PageHistoryDiff.js'; 3 | import PageHistoryList from './PageHistoryList.js'; 4 | import PageHistorySettings from './PageHistorySettings.js'; 5 | 6 | import 'whatwg-fetch'; 7 | 8 | export default class PageHistory extends React.Component { 9 | 10 | constructor( props ) { 11 | super( props ); 12 | this.state = { 13 | revisions: this.props.revisions, 14 | diff: { a: null, b: null }, 15 | page: 1, 16 | hasMore: true, 17 | loading: false, 18 | }; 19 | } 20 | 21 | render() { 22 | 23 | var actions = { 24 | onSelectRevision: ( revision ) => { this.onSelectRevision( revision ) }, 25 | onFetchRevisions: () => this.onfetchRevisions(), 26 | onClearDiff: () => { this.onClearDiff() }, 27 | }; 28 | 29 | return
    30 | 31 | 32 |
    33 | 34 | } 35 | 36 | onfetchRevisions() { 37 | 38 | if ( ! this.state.hasMore || this.state.loading ) { 39 | return; 40 | } 41 | 42 | this.setState( { loading: true } ); 43 | 44 | var url = PageHistorySettings.api_base + 'revisions/' + this.props.post_id + '/?paged=' + this.state.page; 45 | 46 | var request = new Request( url, { 47 | credentials: 'include', 48 | headers: new Headers({ 49 | 'X-WP-Nonce': PageHistorySettings.api_nonce 50 | }) 51 | }); 52 | 53 | return fetch( request ).then( response => { 54 | if ( response.ok ) { 55 | return response.json(); 56 | } 57 | }).then( json => { 58 | this.setState( { 59 | revisions: this.state.revisions.concat( json.revisions ), 60 | page: this.state.page += 1, 61 | loading: false, 62 | hasMore: json.hasMore, 63 | }); 64 | }); 65 | 66 | } 67 | 68 | /** 69 | * Get the revision to compare against. 70 | * This should always be the next most recent revision. 71 | */ 72 | getNextRevision( revision ) { 73 | 74 | var currentIndex, revision_b; 75 | 76 | if ( ! revision ) { 77 | return null; 78 | } 79 | 80 | this.state.revisions.forEach( ( _revision, i ) => { 81 | if ( _revision.id === revision.id ) { 82 | currentIndex = i; 83 | } 84 | }); 85 | 86 | if ( 'undefined' === typeof currentIndex ) { 87 | return null; 88 | } 89 | 90 | if ( currentIndex < this.state.revisions.length - 1 ) { 91 | revision_b = this.state.revisions[ currentIndex + 1 ]; 92 | } else { 93 | revision_b = this.state.revisions[ this.state.revisions.length - 1 ]; 94 | } 95 | 96 | return revision_b; 97 | } 98 | 99 | onSelectRevision( revision ) { 100 | 101 | // Update active state for each revision. 102 | var newRevisions = this.state.revisions.map( ( _revision ) => { 103 | _revision.active = revision && revision.id === _revision.id; 104 | return _revision; 105 | }); 106 | 107 | var revision_b = this.getNextRevision( revision ); 108 | 109 | this.setState({ 110 | revisions: newRevisions, 111 | diff: { 112 | a: revision.content, 113 | b: revision_b.content 114 | }, 115 | }); 116 | 117 | // Defer until actually set. 118 | this.toggleContainerClass(); 119 | } 120 | 121 | onClearDiff() { 122 | 123 | // Update active state for each revision. 124 | var newRevisions = this.state.revisions.map( ( _revision ) => { 125 | _revision.active = false 126 | return _revision; 127 | }); 128 | 129 | this.setState({ 130 | revisions: newRevisions, 131 | diff: { a: null, b: null }, 132 | }); 133 | 134 | this.toggleContainerClass(); 135 | } 136 | 137 | toggleContainerClass() { 138 | window.setTimeout( () => { 139 | if ( null === this.state.diff.a || null === this.state.diff.b ) { 140 | this.props.containerEl.classList.remove( 'article-showing-diff' ); 141 | } else { 142 | this.props.containerEl.classList.add( 'article-showing-diff' ); 143 | } 144 | } ); 145 | } 146 | } 147 | 148 | PageHistory.propTypes = { 149 | post_id: React.PropTypes.number.isRequired, 150 | post_type: React.PropTypes.string, 151 | revisions: React.PropTypes.array, 152 | containerEl: React.PropTypes.object, // DOM element. 153 | }; 154 | 155 | PageHistory.defaultProps = { 156 | revisions: [], 157 | post_type: 'post', 158 | }; 159 | -------------------------------------------------------------------------------- /inc/primary-nav.php: -------------------------------------------------------------------------------- 1 | 'menu_order, title', 20 | 'parent' => $parent_id, 21 | 'post_status' => is_user_logged_in() ? [ 'private', 'publish' ] : 'publish', 22 | ]) ; 23 | 24 | if ( empty( $pages ) ) { 25 | return; 26 | } 27 | 28 | $classes = []; 29 | 30 | if ( 0 === $parent_id ) { 31 | $classes[] = 'NavAccordion'; 32 | } 33 | 34 | printf( 35 | '
      ', 36 | esc_attr( implode( ' ', array_map( 'sanitize_html_class', $classes ) ) ) 37 | ); 38 | 39 | array_walk( $pages, __NAMESPACE__ . '\\render_nav_item' ); 40 | 41 | echo '
    '; 42 | 43 | } 44 | 45 | /** 46 | * Output a single page tree navigation list item. 47 | * 48 | * @param WP_Post $page 49 | * @return null 50 | */ 51 | function render_nav_item( \WP_Post $page ) { 52 | 53 | $classes = ['NavAccordion_Item']; 54 | 55 | if ( 'private' === get_post_status( $page->ID ) ) { 56 | $classes[] = 'Nav_Item-Private'; 57 | } 58 | 59 | if ( is_nav_item_current( $page ) ) { 60 | $classes[] = 'NavAccordion_Item-Active'; 61 | } 62 | 63 | printf( 64 | '
  • %s', 65 | esc_attr( implode( ' ', array_map( 'sanitize_html_class', $classes ) ) ), 66 | esc_url( get_permalink( $page->ID ) ), 67 | esc_html( $page->post_title ) 68 | ); 69 | 70 | render_nav_list( $page->ID ); 71 | 72 | echo '
  • '; 73 | } 74 | 75 | function is_nav_item_current( \WP_Post $page ) { 76 | 77 | $page_url = get_permalink( $page->ID ); 78 | 79 | if ( parse_url( $page_url, PHP_URL_HOST ) !== $_SERVER['HTTP_HOST'] ) { 80 | return false; 81 | } 82 | 83 | if ( parse_url( $page_url, PHP_URL_PATH ) !== $_SERVER['REQUEST_URI'] ) { 84 | return false; 85 | } 86 | 87 | return true; 88 | 89 | } 90 | 91 | /** 92 | * Add nav accordion menu item class 93 | * 94 | * Filters nav_menu_css_class for nav-primary 95 | * 96 | * @param array $classes Menu item classes 97 | * @param WP_Post $item Menu item object 98 | * @param stdClass $args Nav menu args 99 | * 100 | * @return array Menu item classes 101 | */ 102 | function nav_accordion_item_class( $classes, $item, $args ) { 103 | if ( 'nav-primary' === $args->theme_location ) { 104 | $classes[] = 'NavAccordion_Item'; 105 | if ( array_intersect( $classes, [ 'current-menu-item' ] ) ) { 106 | $classes[] = 'NavAccordion_Item-Active'; 107 | } 108 | } 109 | return $classes; 110 | } 111 | 112 | /** 113 | * Add nav accordion menu item class 114 | * 115 | * Filters nav_menu_link_attributes for nav-primary 116 | * 117 | * @param array $atts Menu item attributes 118 | * @param WP_Post $item Menu item object 119 | * @param stdClass $args Nav menu args 120 | * 121 | * @return array Menu item attributes 122 | */ 123 | function nav_accordion_link_attributes( $atts, $item, $args ) { 124 | if ( 'nav-primary' === $args->theme_location ) { 125 | $atts['class'] = isset( $atts['class'] ) ? $atts['class'] : ''; 126 | $atts['class'] .= ' NavAccordion_Anchor'; 127 | } 128 | return $atts; 129 | } 130 | 131 | /** 132 | * Add nav private item class 133 | * 134 | * Filter nav_menu_css_class for private content 135 | * 136 | * @param array $classes Menu item classes 137 | * @param WP_Post $item Menu item object 138 | * @param stdClass $args Nav menu args 139 | * 140 | * @return array Menu item classes 141 | */ 142 | function nav_private_item_class( $classes, $item, $args ) { 143 | if ( ! empty( $item->object_id ) && 'private' === get_post_status( $item->object_id ) ) { 144 | $classes[] = 'Nav_Item-Private'; 145 | } 146 | return $classes; 147 | } 148 | 149 | /** 150 | * Remove private items from menus when logged out. 151 | */ 152 | function nav_private_link_remove( $items, $menu, $args ) { 153 | if ( ! is_user_logged_in() ) { 154 | array_walk( $items, function( $item, $key ) use ( &$items ) { 155 | if ( 156 | ! empty( $item->object_id ) 157 | && 'private' === get_post_status( $item->object_id ) 158 | ) { 159 | unset( $items[ $key ] ); 160 | } 161 | } ); 162 | } 163 | return $items; 164 | } 165 | -------------------------------------------------------------------------------- /inc/page-history.php: -------------------------------------------------------------------------------- 1 | [ 52 | 'listTitle' => __( 'Page History', 'hm-handbook' ), 53 | 'loadMore' => __( 'Load more revisions', 'hm-handbook' ), 54 | ], 55 | 'post_id' => get_the_ID(), 56 | 'api_base' => rest_url( 'hm-handbook/v1/' ), 57 | 'api_nonce' => wp_create_nonce( 'wp_rest' ), 58 | ] ); 59 | 60 | } 61 | 62 | /** 63 | * Setup API. Register rest routes. 64 | * 65 | * @return null 66 | */ 67 | function setup_api() { 68 | register_rest_route( 69 | 'hm-handbook/v1', 70 | '/revisions/(?P\d+)', 71 | [ 72 | 'methods' => 'GET', 73 | 'callback' => __NAMESPACE__ . '\\get_revisions_response', 74 | 'permission_callback' => __NAMESPACE__ . '\\revisions_request_permissions_callback', 75 | 'args' => [ 76 | 'id' => [ 77 | 'sanitize_callback' => 'absint', 78 | 'validate_callback' => function( $param, $request, $key ) { 79 | return is_numeric( $param ); 80 | } 81 | ], 82 | 'paged' => [ 83 | 'sanitize_callback' => 'absint', 84 | 'validate_callback' => function( $param, $request, $key ) { 85 | return is_numeric( $param ); 86 | } 87 | ], 88 | ], 89 | ] 90 | ); 91 | 92 | } 93 | 94 | /** 95 | * Get post revisions API request response. 96 | * 97 | * @param \WP_REST_Request $request Request 98 | * 99 | * @return \WP_REST_Response Response 100 | */ 101 | function get_revisions_response( WP_REST_Request $request ) { 102 | 103 | $revisions = []; 104 | 105 | $query = new WP_Query( [ 106 | 'post_parent' => $request->get_param( 'id' ), 107 | 'post_type' => 'revision', 108 | 'post_status' => 'inherit', 109 | 'posts_per_page' => 5, 110 | 'paged' => $request->get_param( 'paged' ), 111 | ] ); 112 | 113 | foreach ( $query->posts as $revision ) { 114 | 115 | $date = new DateTime( $revision->post_modified_gmt ); 116 | $author = get_userdata( $revision->post_author ); 117 | 118 | $revisions[] = [ 119 | 'id' => $revision->ID, 120 | 'content' => $revision->post_content, 121 | 'date' => $date->format( 'j M y @ H:i' ), 122 | 'author' => get_the_author_meta( 'display_name', $revision->post_author ), 123 | ]; 124 | 125 | }; 126 | 127 | return rest_ensure_response( array( 128 | 'revisions' => $revisions, 129 | 'hasMore' => $request->get_param( 'paged' ) < $query->max_num_pages, 130 | ) ); 131 | 132 | } 133 | 134 | /** 135 | * Request permissions callback. 136 | * 137 | * Revisions of published posts are public. Otherwise they fall back to the edit_post cap. 138 | * 139 | * @param WP_REST_Request $request Full data about the request. 140 | * @return WP_Error|boolean 141 | */ 142 | function revisions_request_permissions_callback( $request ) { 143 | 144 | $post_id = $request->get_param( 'id' ); 145 | 146 | if ( 'publish' === get_post_status( $post_id ) ) { 147 | return true; 148 | } 149 | 150 | $parent = apply_filters( 'rest_the_post', get_post( $post_id ), $post_id ); 151 | 152 | if ( ! $parent ) { 153 | return true; 154 | } 155 | 156 | $parent_post_type_obj = get_post_type_object( $parent->post_type ); 157 | 158 | if ( ! current_user_can( $parent_post_type_obj->cap->edit_post, $parent->ID ) ) { 159 | return new WP_Error( 'rest_cannot_read', __( 'Sorry, you cannot view revisions of this post.' ), array( 'status' => rest_authorization_required_code() ) ); 160 | } 161 | 162 | return true; 163 | 164 | } 165 | -------------------------------------------------------------------------------- /assets/src/styles/components/_nav-accordion.scss: -------------------------------------------------------------------------------- 1 | /** 2 | * Helper mixin for generating color variations. 3 | */ 4 | @mixin NavAccordion_Coloured( $bg, $border-color, $text-color: $color-text-default ) { 5 | 6 | background: $bg; 7 | color: $text-color; 8 | 9 | .NavAccordion_Item { 10 | border-color: $border-color; 11 | } 12 | 13 | .NavAccordion_Item > ul:after { 14 | border-color: $border-color; 15 | } 16 | 17 | .NavAccordion_Item .NavAccordion_Item { 18 | .NavAccordion_Anchor:after { 19 | border-color: $border-color; 20 | } 21 | } 22 | 23 | $border-color-toggle: $border-color; 24 | $border-color-toggle-hover: $border-color; 25 | 26 | @if ( lightness( $bg ) > 60 ) { 27 | $border-color-toggle: darken( $border-color, 20% ); 28 | $border-color-toggle-hover: darken( $border-color, 40% ); 29 | } @else { 30 | $border-color-hover: lighten( $border-color, 30% ); 31 | $border-color-toggle-hover: lighten( $border-color, 60% ); 32 | } 33 | 34 | .NavAccordion_Toggle { 35 | 36 | border-color: $border-color-toggle; 37 | color: $border-color-toggle; 38 | 39 | &:hover, 40 | &:focus { 41 | border-color: $border-color-toggle-hover; 42 | color: $border-color-toggle-hover; 43 | } 44 | 45 | } 46 | 47 | } 48 | 49 | .NavAccordion { 50 | 51 | @include font-heading; 52 | @include text-sm; 53 | line-height: $base-line-height * .75; 54 | list-style: none; 55 | margin: 0; 56 | padding: 0; 57 | 58 | a:link, 59 | a:visited { 60 | color: inherit; 61 | border: none; 62 | } 63 | 64 | a:hover, 65 | a:focus { 66 | border: none; 67 | } 68 | 69 | ul, 70 | ol { 71 | list-style: none; 72 | font-size: inherit; 73 | margin: 0; 74 | padding: 0; 75 | position: relative; 76 | } 77 | 78 | } 79 | 80 | .NavAccordion_Item { 81 | 82 | font-size: inherit; 83 | position: relative; 84 | display: block; 85 | border-bottom: 1px solid $border-color; 86 | 87 | &:last-child { 88 | border-bottom: none; 89 | } 90 | 91 | > ul { 92 | overflow: hidden; 93 | margin-left: $gutter-width * 0.5; 94 | } 95 | 96 | } 97 | 98 | .NavAccordion_Anchor { 99 | display: inline-block; 100 | padding: 7.5px 0; 101 | position: relative; 102 | } 103 | 104 | .NavAccordion_Item > ul:after { 105 | content: " "; 106 | border-left: 1px solid $border-color; 107 | top: -.333333333rem; 108 | bottom: calc( 0.9rem - 1px ); 109 | left: 0; 110 | position: absolute; 111 | } 112 | 113 | .NavAccordion_Item .NavAccordion_Item { 114 | 115 | margin-left: 0; 116 | padding-left: $gutter-width; 117 | border-left: none; 118 | border-bottom: none; 119 | 120 | .NavAccordion_Anchor:after { 121 | content: " "; 122 | width: $gutter-width * 0.5; 123 | border-bottom: 1px solid $border-color; 124 | position: absolute; 125 | top: 19px; 126 | left: $gutter-width * -1; 127 | } 128 | 129 | > ul { 130 | margin-left: 0; 131 | } 132 | 133 | } 134 | 135 | .NavAccordion_Item .NavAccordion_Item .NavAccordion_Item { 136 | @include font-body; 137 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif; 138 | } 139 | 140 | .NavAccordion_Toggle { 141 | 142 | @extend .btn--small; 143 | border: none; 144 | display: block; 145 | position: absolute; 146 | right: 0; 147 | top: 0; 148 | height: $base-line-height * 1.25; 149 | width: $base-line-height * 1.25; 150 | background: transparent; 151 | 152 | &:after { 153 | @include icon( "plus-alt" ); 154 | position: absolute; 155 | content: " "; 156 | top: 50%; 157 | left: 50%; 158 | width: 18px; 159 | height: 18px; 160 | background-size: 100% 100%; 161 | margin-left: -9px; 162 | margin-top: -9px; 163 | text-indent: 0; 164 | opacity: 0.75; 165 | } 166 | 167 | &:hover, 168 | &:focus { 169 | background: none; 170 | border: none; 171 | 172 | &:after { 173 | opacity: 1; 174 | } 175 | } 176 | 177 | &:focus { 178 | outline: none; 179 | } 180 | 181 | } 182 | 183 | .NavAccordion_Toggle-Open:after { 184 | $iconSrc: iconSrc( "minus-alt" ); 185 | background-image: url( $iconSrc ); 186 | } 187 | 188 | .NavAccordion-Red { 189 | @include NavAccordion_Coloured( $color-primary, $border-color-primary, $color-nav-accordion-primary-text ); 190 | 191 | .NavAccordion_Toggle { 192 | 193 | &:before { 194 | background: rgba( 255, 255, 255, .85 ); 195 | } 196 | 197 | &:after { 198 | $iconSrc: iconSrc( "plus-alt", "white" ); 199 | background-image: url( $iconSrc ); 200 | } 201 | 202 | &:hover:before, 203 | &:focus:before { 204 | background-color: $color-nav-accordion-primary-focus-background; 205 | } 206 | 207 | } 208 | 209 | .NavAccordion_Toggle-Open:after { 210 | $iconSrc: iconSrc( "minus-alt", "white" ); 211 | background-image: url( $iconSrc ); 212 | } 213 | 214 | } 215 | 216 | .NavAccordion-Grey { 217 | @include NavAccordion_Coloured( 218 | $hm-light-grey, 219 | $border-color-light, 220 | $hm-medium-grey 221 | ); 222 | } 223 | 224 | .NavAccordion_Item-Active > .NavAccordion_Anchor { 225 | text-decoration: underline; 226 | } 227 | -------------------------------------------------------------------------------- /assets/src/styles/components/_PageHistory.scss: -------------------------------------------------------------------------------- 1 | 2 | @use "sass:math"; 3 | 4 | .PageHistory_Diff-Added { 5 | background: $color-success; 6 | text-decoration: none; 7 | } 8 | 9 | .PageHistory_Diff-Removed { 10 | background: lighten( $color-primary, 35% ); 11 | } 12 | 13 | .PageHistory_List_Container { 14 | 15 | width: 100%; 16 | position: relative; 17 | margin: $base-line-height 0; 18 | padding: #{ $base-line-height * 0.5 } #{ $gutter-width }; 19 | background: $color-pagehistory-background; 20 | border-radius: 2px; 21 | 22 | @media #{ $mq-md-up } { 23 | width: 250px; 24 | position: absolute; 25 | left: 100%; 26 | top: 0; 27 | margin: 0 $gutter-width; 28 | } 29 | 30 | @media #{ $mq-lg-up } { 31 | margin: 0 $gutter-width * 2; 32 | } 33 | 34 | @media ( min-width: 1600px ) { 35 | margin: 0 #{ $gutter-width * 2 }; 36 | } 37 | 38 | } 39 | 40 | .PageHistory_List_Container { 41 | .PageHistory_List + .btn .Loading { 42 | background-image: url( "#{ $images-path }/icons/icon-spinner-black.svg" ); 43 | background-size: 50%; 44 | margin-right: 7px; 45 | opacity: 0.3; 46 | } 47 | } 48 | 49 | .PageHistory_List_Container-Collapsed { 50 | 51 | height: 3rem; 52 | overflow: hidden; 53 | 54 | .PageHistory_List, 55 | .PageHistory_List + .btn { 56 | display: none; 57 | } 58 | 59 | } 60 | 61 | .PageHistory_List_Container-Expanded { 62 | 63 | height: auto; 64 | 65 | .PageHistory_List, 66 | .PageHistory_List + .btn { 67 | display: block; 68 | 69 | &[disabled] { 70 | display: none; 71 | } 72 | 73 | } 74 | 75 | } 76 | 77 | .PageHistory_List_Title { 78 | @include text-sm; 79 | margin-top: 0; 80 | margin-bottom: 0; 81 | } 82 | 83 | .PageHistory_List { 84 | 85 | @include text-sm; 86 | color: $color-pagehistory-list; 87 | list-style: none; 88 | position: relative; 89 | margin: #{ $base-line-height * 0.25 } 0; 90 | padding-left: #{ $gutter-width * 0.5 }; 91 | 92 | &:after { 93 | content: " "; 94 | display: block; 95 | border-left: 1px solid $border-color-darken; 96 | position: absolute; 97 | top: $base-line-height * 0.5; 98 | bottom: calc( #{ $base-line-height * 0.5 } + 2px ); 99 | left: $gutter-width * 0.25 - 1px; 100 | } 101 | 102 | & + .btn { 103 | margin: 0; 104 | padding: 0; 105 | } 106 | 107 | } 108 | 109 | .PageHistory_List_Item { 110 | 111 | position: relative; 112 | padding: 0 0 0 #{ $gutter-width * 0.5 }; 113 | 114 | a { 115 | padding: #{ $base-line-height * 0.125 } 0; 116 | display: block; 117 | } 118 | 119 | a:link, 120 | a:visited { 121 | color: inherit; 122 | text-decoration: inherit; 123 | border: none; 124 | } 125 | 126 | a:hover { 127 | color: #444; 128 | text-decoration: underline; 129 | border: none; 130 | } 131 | 132 | &:after { 133 | content: " "; 134 | display: block; 135 | width: 5px; 136 | height: 5px; 137 | background: $border-color-darken; 138 | border-radius: 100%; 139 | position: absolute; 140 | left: math.div($gutter-width, -4) - 3px; 141 | top: $base-line-height * 0.5; 142 | margin-top: 0; 143 | z-index: 1; 144 | } 145 | 146 | &:first-child:after, 147 | &.PageHistory_List_Item-Active:after { 148 | width: 9px; 149 | height: 9px; 150 | margin-top: -3px; 151 | left: math.div($gutter-width, -4) - 5px; 152 | } 153 | 154 | &.PageHistory_List_Item-Active:after { 155 | background-color: $color-primary; 156 | } 157 | 158 | &.PageHistory_List_Item-Active + .PageHistory_List_Item:after { 159 | background-color: $color-pagehistory-list; 160 | } 161 | } 162 | 163 | .PageHistory_Diff { 164 | @include text-sm; 165 | display: none; // Only shown when active. 166 | white-space: pre-wrap; 167 | background: $color-pagehistory-background; 168 | padding: $base-line-height * 0.5 $gutter-width * 0.5; 169 | font-family: $font-family-code; 170 | word-wrap: break-word; 171 | } 172 | 173 | 174 | .article-showing-diff { 175 | 176 | .article-content { 177 | display: none; 178 | } 179 | 180 | .PageHistory_Diff { 181 | display: block; 182 | } 183 | 184 | } 185 | 186 | .btn--toggle { 187 | 188 | background: none; 189 | border: none; 190 | position: absolute; 191 | top: .7775rem; 192 | right: $gutter-width; 193 | margin: 0; 194 | text-indent: 100%; 195 | overflow: hidden; 196 | white-space: nowrap; 197 | padding: 0; 198 | width: 1.333rem; 199 | height: 1.333rem; 200 | 201 | &:after { 202 | position: absolute; 203 | content: " "; 204 | background: url( "#{ $images-path }/icons/icon-plus-alt-black.svg" ) no-repeat center center; 205 | background-size: 100% 100%; 206 | top: 50%; 207 | left: 50%; 208 | width: 18px; 209 | height: 18px; 210 | margin-left: -9px; 211 | margin-top: -9px; 212 | text-indent: 0; 213 | } 214 | 215 | &:hover, 216 | &:focus { 217 | 218 | background: none; 219 | outline: none; 220 | 221 | &:before { 222 | background: darken( $color-nav-toggle-background, 40% ); 223 | } 224 | } 225 | 226 | 227 | &.btn--state-expanded:after { 228 | background-image: url( "#{ $images-path }/icons/icon-minus-alt-black.svg" ); 229 | } 230 | 231 | } 232 | 233 | .btn--print { 234 | display: flex; 235 | align-items: center; 236 | 237 | > svg { 238 | width: 18px; 239 | height: 18px; 240 | margin-right: 0.5em; 241 | } 242 | 243 | @media print { 244 | display: none; 245 | } 246 | } 247 | -------------------------------------------------------------------------------- /assets/src/styles/components/_header.scss: -------------------------------------------------------------------------------- 1 | .site-header { 2 | 3 | @include clearfix; 4 | background-color: $color-primary; 5 | padding: 0; 6 | display: flex; 7 | flex-wrap: wrap; 8 | justify-content: left; 9 | 10 | } 11 | 12 | .site-logo { 13 | 14 | width: 100%; 15 | margin: 0 auto; 16 | 17 | h1 { 18 | margin: 0; 19 | padding: 0; 20 | } 21 | 22 | .hm-logo { 23 | border: none; 24 | display: block; 25 | width: 210px; 26 | height: 40px; 27 | margin: calc( ( #{ $base-line-height * 2 } - 40px ) / 2 ) auto; 28 | margin-top: calc( #{ $base-line-height * 0.5 } + ( ( #{ $base-line-height * 2 } - 40px ) / 2 ) ); 29 | 30 | &:hover { 31 | border: none; 32 | } 33 | 34 | @media #{ $mq-md-down } { 35 | margin-left: auto; 36 | margin-right: auto; 37 | } 38 | 39 | } 40 | 41 | .site-title { 42 | @include font-heading; 43 | @include text-sm; 44 | color: $white; 45 | text-align: center; 46 | position: relative; 47 | } 48 | 49 | @media #{ $mq-md-up } { 50 | width: 300px; 51 | } 52 | 53 | @media print { 54 | display: flex; 55 | align-items: center; 56 | 57 | .hm-logo { 58 | margin: 1em 0 1em 2em; 59 | height: 1.4em; 60 | width: auto; 61 | } 62 | } 63 | } 64 | 65 | .site-search { 66 | 67 | display: block; 68 | width: 100%; 69 | 70 | @media #{ $mq-md-up } { 71 | width: auto; 72 | flex: 1; 73 | } 74 | 75 | .search-container { 76 | width: 100%; 77 | clear: both; 78 | padding: 1.5rem 30px; 79 | max-width: 900px; 80 | float: right; 81 | 82 | @media #{ $mq-md-down } { 83 | padding-top: 1rem; 84 | } 85 | } 86 | 87 | @media print { 88 | display: none; 89 | } 90 | 91 | } 92 | 93 | .search-bar { 94 | @extend .util-clearfix; 95 | clear: both; 96 | position: relative; 97 | 98 | .search-bar__container { 99 | @extend .util-clearfix; 100 | position: relative; 101 | 102 | @media #{ $mq-md-up } { 103 | width: 50%; 104 | float: right; 105 | transition: width ease-in-out .1s; 106 | } 107 | } 108 | 109 | .search-bar__label { 110 | position: absolute; 111 | clip: rect( 1px, 1px, 1px, 1px ); 112 | } 113 | 114 | .search-bar__field { 115 | border-color: var( --hm-dark-grey ); 116 | background: var( --hm-light-grey ); 117 | color: var( --hm-dark-grey ); 118 | float: right; 119 | margin: 0; 120 | padding-right: 0; 121 | width: 100%; 122 | height: $base-line-height * 2; 123 | transition: color .2s, border-color .2s, background .2s; 124 | 125 | &::placeholder { 126 | color: white; 127 | } 128 | 129 | &:placeholder-shown { 130 | border-color: rgba( 255, 255, 255, 0.8 ); 131 | background: transparent; 132 | 133 | + .search-bar__submit::after { 134 | $iconSrc: iconSrc( "search", "white" ); 135 | background: url( $iconSrc ) center center no-repeat; 136 | } 137 | } 138 | 139 | &:focus { 140 | background: var( --hm-light-grey ); 141 | border-color: var( --hm-dark-grey ); 142 | color: var( --hm-dark-grey ); 143 | 144 | &::placeholder { 145 | color: var( --hm-dark-grey ); 146 | } 147 | 148 | + .search-bar__submit::after { 149 | $iconSrc: iconSrc( "search", "black" ); 150 | background: url( $iconSrc ) center center no-repeat; 151 | } 152 | } 153 | } 154 | 155 | .search-bar__submit { 156 | @extend .btn; 157 | background: none; 158 | border: none; 159 | position: absolute; 160 | top: 0; 161 | right: 0; 162 | margin: 0; 163 | text-indent: 150%; 164 | padding: 0; 165 | height: $base-line-height * 2; 166 | width: $base-line-height * 2; 167 | overflow: hidden; 168 | opacity: .75; 169 | 170 | &::after { 171 | $iconSrc: iconSrc( "search", "black" ); 172 | background: url( $iconSrc ) center center no-repeat; 173 | content: " "; 174 | display: block; 175 | position: absolute; 176 | width: 100%; 177 | height: 100%; 178 | top: 0; 179 | left: 0; 180 | } 181 | 182 | &:hover, 183 | &:focus { 184 | background: none; 185 | border: none; 186 | } 187 | } 188 | 189 | .search-bar__results { 190 | display: none; 191 | position: absolute; 192 | top: 100%; 193 | left: 0; 194 | right: 0; 195 | border: 2px solid var( --hm-dark-grey ); 196 | border-top-color: transparent; 197 | background: var( --hm-light-grey ); 198 | padding: 0; 199 | border-radius: 0 0 2px 2px; 200 | color: var( --hm-dark-grey ); 201 | margin: 0; 202 | list-style: none; 203 | z-index: 50; 204 | overflow-x: hidden; 205 | } 206 | 207 | .search-bar__result { 208 | 209 | @include text-sm; 210 | 211 | padding: 0; 212 | margin: 0; 213 | 214 | a { 215 | border: none; 216 | text-decoration: none; 217 | } 218 | 219 | a:link { 220 | padding: #{ $base-line-height * 0.5 } #{ $gutter-width }; 221 | margin: 0; 222 | display: block; 223 | 224 | &:focus, 225 | &:hover { 226 | background: rgba( 0, 0, 0, .1 ); 227 | outline: none; 228 | transition: none; 229 | } 230 | } 231 | } 232 | 233 | .search-bar__result + .search-bar__result { 234 | border-top: 1px solid var( --hm-dark-grey ); 235 | } 236 | 237 | .search-bar__results__info, 238 | .search-bar__result__title, 239 | .search-bar__result__text { 240 | @include text-sm; 241 | color: var( --hm-dark-grey ); 242 | margin: 0; 243 | } 244 | 245 | .search-bar__results__info { 246 | border-top: 1px solid var( --hm-dark-grey ); 247 | padding: #{ $base-line-height * 0.5 } #{ $gutter-width }; 248 | } 249 | 250 | .search-bar__result__title { 251 | margin: 0 0 #{ $gutter-width * 0.5 } 0; 252 | } 253 | 254 | &.search-bar--focused, 255 | &.search-bar--has-results:focus-within { 256 | 257 | @media #{ $mq-md-up } { 258 | .search-bar__container { 259 | width: 100%; 260 | transition: width ease-in-out .1s; 261 | } 262 | } 263 | } 264 | 265 | &.search-bar--has-results:focus-within { 266 | .search-bar__field { 267 | border-bottom: none; 268 | border-bottom-left-radius: 0; 269 | border-bottom-right-radius: 0; 270 | } 271 | 272 | .search-bar__results { 273 | display: block; 274 | } 275 | } 276 | } 277 | -------------------------------------------------------------------------------- /inc/search.php: -------------------------------------------------------------------------------- 1 | rest_url( 'hm-handbook/v1/search' ), 13 | 'api_nonce' => wp_create_nonce( 'wp_rest' ), 14 | 'strings' => [ 15 | 'label' => __( 'Search', 'hm-handbook' ), 16 | 'button' => __( 'Submit', 'hm-handbook' ), 17 | 'placeholder' => __( 'Search the site…', 'hm-handbook' ), 18 | 'noResults' => __( 'No results found', 'hm-handbook' ), 19 | ], 20 | ] ); 21 | 22 | }, 20 ); 23 | 24 | add_action( 'rest_api_init', function() { 25 | 26 | register_rest_route( 'hm-handbook/v1', '/search', [ 27 | 'callback' => __NAMESPACE__ . '\\search_request_callback', 28 | 'methods' => WP_REST_Server::READABLE, 29 | 'args' => [ 30 | 'query' => [ 31 | 'sanitize_callback' => 'sanitize_text_field', 32 | ], 33 | ], 34 | ] ); 35 | 36 | } ); 37 | 38 | class Result { 39 | public $title; 40 | public $excerpt; 41 | public $site; 42 | public $author; 43 | public $date; 44 | public $url; 45 | public $type; 46 | } 47 | 48 | function search_request_callback( WP_Rest_Request $request ) { 49 | 50 | $query = $request->get_param( 'query' ); 51 | $data = fallback_search_request( $query ); 52 | 53 | return rest_ensure_response( $data ); 54 | 55 | } 56 | 57 | /** 58 | * Do the elastic search request. 59 | * 60 | * @param string $query Query string. 61 | * 62 | * @return array Array of HM_Handbook\Search\Result objects. 63 | */ 64 | function elastic_search_request( $query ) { 65 | 66 | $args = [ 67 | 'sites' => [ get_current_blog_id() ], 68 | ]; 69 | 70 | $items = hmn_hmes_search_items( trim( $query ), $args ); 71 | 72 | $data = [ 73 | 'query' => $query, 74 | 'results' => [], 75 | ]; 76 | 77 | foreach ( $items as $item ) { 78 | switch ( $item['_type'] ) { 79 | case 'post': 80 | $data['results'][] = normalize_post( $item ); 81 | break; 82 | 83 | case 'comment' : 84 | $data['results'][] = normalize_comment( $item ); 85 | break; 86 | } 87 | } 88 | 89 | return $data; 90 | } 91 | 92 | 93 | /** 94 | * Fallback to standard WP search if no elastic search is found. 95 | * 96 | * Note only returns top 50 results. No pagination is supported. 97 | * 98 | * @param string $query Query string. 99 | * 100 | * @return array Array of HM_Handbook\Search\Result objects. 101 | */ 102 | function fallback_search_request( $query ) { 103 | 104 | $search_query_args = [ 105 | 's' => $query, 106 | 'post_type' => get_post_types( [ 'public' => true ] ), 107 | 'posts_per_page' => 50, 108 | 'post_status' => [ 'publish' ], 109 | 'perm' => 'readable', 110 | ]; 111 | 112 | if ( is_user_logged_in() ) { 113 | $search_query_args['post_status'][] = 'private'; 114 | } 115 | 116 | $search_query = new WP_Query( $search_query_args ); 117 | $results = []; 118 | 119 | while ( $search_query->have_posts() ) { 120 | 121 | $search_query->the_post(); 122 | 123 | $date_format = sprintf( '%s @ %s', get_option( 'date_format' ), get_option( 'time_format' ) ); 124 | 125 | $result = new Result; 126 | $result->title = strip_tags( html_entity_decode( get_the_title() ) ); 127 | $result->excerpt = strip_tags( html_entity_decode( get_the_excerpt() ) ); 128 | $result->author = get_the_author(); 129 | $result->date = get_the_date( $date_format ); 130 | $result->url = get_permalink(); 131 | 132 | if ( empty( $result->title ) && empty( $result->excerpt ) ) { 133 | continue; 134 | } 135 | 136 | array_push( $results, $result ); 137 | 138 | } 139 | 140 | return [ 141 | 'query' => $query, 142 | 'results' => $results, 143 | ]; 144 | 145 | } 146 | 147 | function normalize_post( $item ) { 148 | $data = new Result(); 149 | 150 | // Grab the site from the index name 151 | $data->site = normalize_site( str_replace( 'hmes_', '', $item['_index'] ) ); 152 | 153 | $post = $item['_source']; 154 | 155 | $data->title = $post['post_title']; 156 | $data->url = $post['guid']; 157 | $data->excerpt = wp_trim_words( $post['post_content'], 80, ' ' . "[\xe2\x80\xa6]" ); 158 | $data->date = $post['post_modified']; 159 | $data->type = isset( $post['post_type'] ) ? $post['post_type'] : 'post'; 160 | 161 | // Author, as per P2's functions 162 | $author = get_user_by( 'id', $post['post_author'] ); 163 | 164 | $cb = function ($caps) { 165 | $caps['list_users'] = true; 166 | return $caps; 167 | }; 168 | 169 | add_filter( 'user_has_cap', $cb ); 170 | $data->author = [ 171 | 'name' => $author->display_name, 172 | 'ID' => $author->ID, 173 | 'avatar' => get_avatar( $author->ID, '48', '', '', [ 'class' => 'search-result-avatar' ] ) 174 | ]; 175 | remove_filter( 'user_has_cap', $cb ); 176 | 177 | $data->raw = $post; 178 | 179 | return $data; 180 | } 181 | 182 | function normalize_comment( $item ) { 183 | $data = new Result(); 184 | 185 | // Grab the site from the index name 186 | $data->site = normalize_site( str_replace( 'hmes_', '', $item['_index'] ) ); 187 | 188 | $post = $item['_source']; 189 | 190 | switch_to_blog( $data->site['id'] ); 191 | 192 | $parent = (array) get_post( $post['comment_post_ID'] ); 193 | 194 | restore_current_blog(); 195 | 196 | $data->title = 'Response to: ' . $parent['post_title']; 197 | $data->url = trailingslashit( $parent['guid'] ) . '#comment-' . $post['comment_ID'] ; 198 | $data->excerpt = wp_trim_words( $post['comment_content'], 80, ' ' . "[\xe2\x80\xa6]" ); 199 | $data->date = $post['comment_date']; 200 | $data->type = 'comment'; 201 | 202 | // Author, as per P2's functions 203 | $author = get_user_by( 'id', $post['user_id'] ); 204 | 205 | $cb = function ($caps) { 206 | $caps['list_users'] = true; 207 | return $caps; 208 | }; 209 | 210 | add_filter( 'user_has_cap', $cb ); 211 | $data->author = [ 212 | 'name' => $author->display_name, 213 | 'ID' => $author->ID, 214 | 'avatar' => get_avatar( $author->ID, '48', '', '', [ 'class' => 'search-result-avatar' ] ) 215 | ]; 216 | remove_filter( 'user_has_cap', $cb ); 217 | 218 | $data->raw = $post; 219 | 220 | return $data; 221 | } 222 | 223 | function normalize_site( $site ) { 224 | $site = get_blog_details( $site ); 225 | 226 | $data = [ 227 | 'id' => $site->blog_id, 228 | 'name' => $site->blogname, 229 | 'domain' => $site->domain, 230 | 'url' => $site->siteurl, 231 | ]; 232 | return $data; 233 | } 234 | -------------------------------------------------------------------------------- /functions.php: -------------------------------------------------------------------------------- 1 | __( 'Content Siedebar (Logged Out)', 'hm-handbook' ), 50 | 'id' => 'site-content-logged-out', 51 | 'description' => __( 'Shown only to logged out visitors.', 'hm-handbook' ), 52 | 'class' => '', 53 | 'before_widget' => '', 55 | 'before_title' => '

    ', 56 | 'after_title' => '

    ' 57 | ] ); 58 | register_sidebar( [ 59 | 'name' => __( 'Footer Content', 'hm-handbook' ), 60 | 'id' => 'footer-content', 61 | 'description' => __( 'Show in the site footer.', 'hm-handbook' ), 62 | 'class' => '', 63 | 'before_widget' => '', 65 | 'before_title' => '

    ', 66 | 'after_title' => '

    ' 67 | ] ); 68 | 69 | // Filter next/prev post classes. 70 | add_filter( 'next_posts_link_attributes', __NAMESPACE__ . '\\posts_link_attributes_next' ); 71 | add_filter( 'previous_posts_link_attributes', __NAMESPACE__ . '\\posts_link_attributes_prev' ); 72 | 73 | add_filter( 'wp_link_pages_link', __NAMESPACE__ . '\\multi_page_links_markup', 10, 2 ); 74 | 75 | } 76 | 77 | /** 78 | * Set up the admin. 79 | */ 80 | function setup_admin() { 81 | 82 | add_editor_style( 'assets/dist/styles/editor.css' ); 83 | 84 | add_filter( 'mce_external_plugins', function( $plugin_array ) { 85 | $plugin_array['typekit'] = get_template_directory_uri() . '/inc/tinyMCE/tinyMCE-typekit.js'; 86 | return $plugin_array; 87 | } ); 88 | 89 | } 90 | 91 | function get_asset_version() { 92 | if ( wp_get_environment_type() === 'local' ) { 93 | return filemtime( __DIR__ . '/assets/dist/styles/theme.css' ); 94 | } 95 | 96 | return wp_get_theme()->Version; 97 | } 98 | 99 | /** 100 | * Add login Styling 101 | */ 102 | add_action( 'login_enqueue_scripts', function() { 103 | wp_enqueue_style( 'hm-login', get_theme_file_uri( 'assets/dist/styles/login.css' ), [], get_asset_version() ); 104 | } ); 105 | 106 | /** 107 | * Enqueue all theme scripts. 108 | */ 109 | function enqueue_scripts() { 110 | $version = get_asset_version(); 111 | wp_enqueue_script( 'hm-handbook', get_theme_file_uri( 'assets/dist/scripts/theme.js' ), [], $version, true ); 112 | wp_enqueue_style( 'hm-handbook', get_theme_file_uri( 'assets/dist/styles/theme.css' ), [], $version ); 113 | 114 | add_action( 'wp_head', function() { 115 | echo ''; 116 | echo ''; 117 | } ); 118 | 119 | if ( is_singular() && comments_open() && get_option( 'thread_comments' ) ) { 120 | wp_enqueue_script( 'comment-reply' ); 121 | } 122 | 123 | } 124 | 125 | /** 126 | * Set the content width in pixels, based on the theme's design and stylesheet. 127 | * 128 | * Priority 0 to make it available to lower priority callbacks. 129 | * 130 | * @global int $content_width 131 | */ 132 | function content_width() { 133 | $GLOBALS['content_width'] = 640; 134 | } 135 | 136 | /** 137 | * Retrieve the URL of a file in the theme. 138 | * 139 | * Searches in the stylesheet directory before the template directory so themes 140 | * which inherit from a parent theme can just override one file. 141 | * 142 | * @param string $file File to search for in the stylesheet directory. 143 | * @return string The URL of the file. 144 | */ 145 | function get_theme_file_uri( $file = '' ) { 146 | $file = ltrim( $file, '/' ); 147 | 148 | if ( empty( $file ) ) { 149 | $url = get_stylesheet_directory_uri(); 150 | } elseif ( file_exists( get_stylesheet_directory() . '/' . $file ) ) { 151 | $url = get_stylesheet_directory_uri() . '/' . $file; 152 | } else { 153 | $url = get_template_directory_uri() . '/' . $file; 154 | } 155 | 156 | return $url; 157 | } 158 | 159 | /** 160 | * Retrieve the URL of a file in the parent theme. 161 | * 162 | * @param string $file File to return the URL for in the template directory. 163 | * @return string The URL of the file. 164 | */ 165 | function get_parent_theme_file_uri( $file = '' ) { 166 | $file = ltrim( $file, '/' ); 167 | 168 | if ( empty( $file ) ) { 169 | $url = get_template_directory_uri(); 170 | } else { 171 | $url = get_template_directory_uri() . '/' . $file; 172 | } 173 | 174 | return $url; 175 | } 176 | 177 | /** 178 | * Add Class to pagination next links 179 | * 180 | * @return string Attributes. 181 | */ 182 | function posts_link_attributes_next() { 183 | return 'class="btn btn--secondary btn--small Pagination-Next"'; 184 | } 185 | 186 | /** 187 | * Add Class to pagination previous links 188 | * 189 | * @return string Attributes. 190 | */ 191 | function posts_link_attributes_prev() { 192 | return 'class="btn btn--secondary btn--small Pagination-Prev"'; 193 | } 194 | 195 | /** 196 | * Add indicative icon to private page titles 197 | * 198 | * @return string Title format. 199 | */ 200 | add_filter( 'private_title_format', function( $format ) { 201 | return '🔒 %s'; 202 | } ); 203 | 204 | /** 205 | * Handle the markup for multi-page posts. 206 | */ 207 | function multi_page_links_markup( $link, $i ) { 208 | 209 | global $page; 210 | 211 | if ( $i === $page ) { 212 | $link = '' . $link . ''; 213 | } 214 | 215 | return $link; 216 | 217 | } 218 | 219 | /** 220 | * Redirect private pages to a hiring page if a user is logged out. 221 | */ 222 | function redirect_private_pages_to_join_page() { 223 | 224 | // Error page - that non logged in users get when accessing private content. 225 | if ( is_404() ) { 226 | 227 | $queried_object = get_queried_object(); 228 | 229 | if ( 230 | isset( $queried_object->post_status ) && 231 | 'private' === $queried_object->post_status && 232 | ! is_user_logged_in() 233 | ) { 234 | wp_safe_redirect( home_url( '/join-human-made/' ) ); 235 | exit(); 236 | } 237 | } 238 | } 239 | 240 | add_action( 'template_redirect', __NAMESPACE__ . '\\redirect_private_pages_to_join_page' ); 241 | --------------------------------------------------------------------------------