├── .dev ├── build-release.sh └── deploy-plugin.sh ├── .distignore ├── .eslintignore ├── .eslintrc.js ├── .github └── workflows │ └── deploy.yml ├── .gitignore ├── README.md ├── changelog.txt ├── composer.json ├── composer.lock ├── guidepost.php ├── package-lock.json ├── package.json ├── phpcs.xml ├── readme.txt ├── src ├── Guidepost.js ├── block.json ├── deprecated.js ├── edit.js ├── guidepost-theme.js ├── icon.js ├── index.js ├── linear-to-nested-list.js ├── save.js └── styles │ ├── editor.scss │ └── style.scss └── webpack.config.js /.dev/build-release.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | PLUGIN="guidepost" 4 | VERSION=$(awk '/Version:/{print $NF}' $PLUGIN.php) 5 | 6 | WORKING_DIR=`pwd` 7 | 8 | rm -rf $WORKING_DIR/release 9 | mkdir -p release/$PLUGIN 10 | 11 | rm -rf $WORKING_DIR/build 12 | npm run build 13 | 14 | rsync -av --progress --exclude={'.*','node_modules','release','src','vendor','wordpress','composer*','package*','phpcs.xml','README.md','webpack*'} $WORKING_DIR/* $WORKING_DIR/release/$PLUGIN 15 | cd $WORKING_DIR/release/ 16 | zip -r "${PLUGIN}-${VERSION}.zip" $PLUGIN 17 | cd $WORKING_DIR/ -------------------------------------------------------------------------------- /.dev/deploy-plugin.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | PLUGIN="guidepost" 4 | VERSION=$(awk '/Version:/{print $NF}' $PLUGIN.php) 5 | WORKING_DIR=`pwd` 6 | 7 | mkdir -p $WORKING_DIR/release/svn 8 | svn co "http://svn.wp-plugins.org/${PLUGIN}" $WORKING_DIR/release/svn 9 | 10 | rm -rf $WORKING_DIR/release/svn/trunk/* 11 | rm -rf $WORKING_DIR/release/svn/assets/* 12 | rsync -av --progress $WORKING_DIR/release/$PLUGIN/* $WORKING_DIR/release/svn/trunk 13 | rsync -av --progress $WORKING_DIR/.wordpress-org/* $WORKING_DIR/release/svn/assets 14 | 15 | cd $WORKING_DIR/release/svn 16 | svn status | grep '^!' | awk '{print $2}' | xargs svn delete 17 | svn add * --force 18 | svn commit -m "Pushing ${VERSION}" 19 | 20 | svn cp trunk tags/$VERSION 21 | svn commit -m "Taggin version ${VERSION}" 22 | 23 | cd $WORKING_DIR 24 | echo "https://downloads.wordpress.org/plugin/${PLUGIN}.${VERSION}.zip" -------------------------------------------------------------------------------- /.distignore: -------------------------------------------------------------------------------- 1 | .* 2 | README.md 3 | composer* 4 | node_modules 5 | package* 6 | phpcs.xml 7 | release 8 | src 9 | vendor 10 | webpack* 11 | wordpress -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | build 2 | node_modules 3 | vendor 4 | wordpress 5 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: [ 3 | 'plugin:@wordpress/eslint-plugin/recommended', 4 | ], 5 | env: { 6 | browser: true, 7 | }, 8 | }; -------------------------------------------------------------------------------- /.github/workflows/deploy.yml: -------------------------------------------------------------------------------- 1 | name: Deploy to WordPress.org 2 | on: 3 | push: 4 | tags: 5 | - "*" 6 | jobs: 7 | tag: 8 | name: New tag 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@master 12 | - name: Build 13 | run: | 14 | npm install 15 | npm run build 16 | - name: WordPress Plugin Deploy 17 | uses: 10up/action-wordpress-plugin-deploy@master 18 | env: 19 | SVN_PASSWORD: ${{ secrets.SVN_PASSWORD }} 20 | SVN_USERNAME: ${{ secrets.SVN_USERNAME }} 21 | SLUG: guidepost 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Mac OS X 2 | .DS_Store 3 | ._* 4 | .Spotlight-V100 5 | .Trashes 6 | 7 | # Windows 8 | Thumbs.db 9 | Desktop.ini 10 | 11 | # Editors 12 | .vscode 13 | .idea 14 | 15 | node_modules/ 16 | vendor/ 17 | wordpress/ 18 | build/ 19 | release/ 20 | 21 | # Sass 22 | .sass-cache 23 | .sass-cache/ 24 | .sass-cache/* 25 | *.map 26 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Guidepost 2 | 3 | A guidepost gives you directions. It lets you know where you’re going. It gives you a preview of what’s to come. 4 | 5 | #### How does it work? 6 | 7 | Guideposts are magic, no they really are. Whenever you add or remove a heading the guidepost block is automatically updated. 8 | 9 | Nesting is done based on which size heading you use. Headings of smaller sizes are automatically nested under ones of larger sizes. 10 | 11 | #### I like this, who made it? 12 | This plugin is brought to you by [sorta brilliant](https://sortabrilliant.com/) and [block garden](https://block.garden). 13 | 14 | ![gp](https://ps.w.org/guidepost/assets/screenshot-1.gif "guidepost") 15 | 16 | -------------------------------------------------------------------------------- /changelog.txt: -------------------------------------------------------------------------------- 1 | 2 | 1.2.1 / 2020-03-22 3 | =================== 4 | 5 | ### Bug Fixes 6 | * Remove extraneous HTML elements in saved output 7 | * Fix visual nesting of elements when editing 8 | 9 | ### Misc 10 | * Bumping "Tested up to" for WordPress 5.4.0 11 | 12 | 1.2.0 / 2020-03-08 13 | =================== 14 | 15 | ### Enhancements 16 | * Performances improvements while editing heading blocks 17 | * Remove jQuery as a dependency 18 | 19 | ### Bug Fixes 20 | * Allow upgrading from versions prior to 1.1.0 with the block name `sbb/guidepost` 21 | * Prevent front-end script from loading within the editor 22 | * Prevent duplicate anchors on headings. 23 | 24 | 25 | 1.1.0 / 2020-02-13 26 | =================== 27 | 28 | ### Misc 29 | * Cleanup for directory submission 30 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sortabrilliant/guidepost", 3 | "type": "wordpress-plugin", 4 | "require": { 5 | "php": ">=5.6" 6 | }, 7 | "config": { 8 | "platform": { 9 | "php": "5.6" 10 | } 11 | }, 12 | "require-dev": { 13 | "squizlabs/php_codesniffer": "^3.5", 14 | "dealerdirect/phpcodesniffer-composer-installer": "^0.5.0", 15 | "phpcompatibility/phpcompatibility-wp": "^2.1", 16 | "wp-coding-standards/wpcs": "^2.1" 17 | }, 18 | "scripts": { 19 | "lint": "@php ./vendor/bin/phpcs" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /composer.lock: -------------------------------------------------------------------------------- 1 | { 2 | "_readme": [ 3 | "This file locks the dependencies of your project to a known state", 4 | "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", 5 | "This file is @generated automatically" 6 | ], 7 | "content-hash": "2601c9443b7c0cda23ecc28822bc2255", 8 | "packages": [], 9 | "packages-dev": [ 10 | { 11 | "name": "dealerdirect/phpcodesniffer-composer-installer", 12 | "version": "v0.5.0", 13 | "source": { 14 | "type": "git", 15 | "url": "https://github.com/Dealerdirect/phpcodesniffer-composer-installer.git", 16 | "reference": "e749410375ff6fb7a040a68878c656c2e610b132" 17 | }, 18 | "dist": { 19 | "type": "zip", 20 | "url": "https://api.github.com/repos/Dealerdirect/phpcodesniffer-composer-installer/zipball/e749410375ff6fb7a040a68878c656c2e610b132", 21 | "reference": "e749410375ff6fb7a040a68878c656c2e610b132", 22 | "shasum": "" 23 | }, 24 | "require": { 25 | "composer-plugin-api": "^1.0", 26 | "php": "^5.3|^7", 27 | "squizlabs/php_codesniffer": "^2|^3" 28 | }, 29 | "require-dev": { 30 | "composer/composer": "*", 31 | "phpcompatibility/php-compatibility": "^9.0", 32 | "sensiolabs/security-checker": "^4.1.0" 33 | }, 34 | "type": "composer-plugin", 35 | "extra": { 36 | "class": "Dealerdirect\\Composer\\Plugin\\Installers\\PHPCodeSniffer\\Plugin" 37 | }, 38 | "autoload": { 39 | "psr-4": { 40 | "Dealerdirect\\Composer\\Plugin\\Installers\\PHPCodeSniffer\\": "src/" 41 | } 42 | }, 43 | "notification-url": "https://packagist.org/downloads/", 44 | "license": [ 45 | "MIT" 46 | ], 47 | "authors": [ 48 | { 49 | "name": "Franck Nijhof", 50 | "email": "franck.nijhof@dealerdirect.com", 51 | "homepage": "http://www.frenck.nl", 52 | "role": "Developer / IT Manager" 53 | } 54 | ], 55 | "description": "PHP_CodeSniffer Standards Composer Installer Plugin", 56 | "homepage": "http://www.dealerdirect.com", 57 | "keywords": [ 58 | "PHPCodeSniffer", 59 | "PHP_CodeSniffer", 60 | "code quality", 61 | "codesniffer", 62 | "composer", 63 | "installer", 64 | "phpcs", 65 | "plugin", 66 | "qa", 67 | "quality", 68 | "standard", 69 | "standards", 70 | "style guide", 71 | "stylecheck", 72 | "tests" 73 | ], 74 | "time": "2018-10-26T13:21:45+00:00" 75 | }, 76 | { 77 | "name": "phpcompatibility/php-compatibility", 78 | "version": "9.3.5", 79 | "source": { 80 | "type": "git", 81 | "url": "https://github.com/PHPCompatibility/PHPCompatibility.git", 82 | "reference": "9fb324479acf6f39452e0655d2429cc0d3914243" 83 | }, 84 | "dist": { 85 | "type": "zip", 86 | "url": "https://api.github.com/repos/PHPCompatibility/PHPCompatibility/zipball/9fb324479acf6f39452e0655d2429cc0d3914243", 87 | "reference": "9fb324479acf6f39452e0655d2429cc0d3914243", 88 | "shasum": "" 89 | }, 90 | "require": { 91 | "php": ">=5.3", 92 | "squizlabs/php_codesniffer": "^2.3 || ^3.0.2" 93 | }, 94 | "conflict": { 95 | "squizlabs/php_codesniffer": "2.6.2" 96 | }, 97 | "require-dev": { 98 | "phpunit/phpunit": "~4.5 || ^5.0 || ^6.0 || ^7.0" 99 | }, 100 | "suggest": { 101 | "dealerdirect/phpcodesniffer-composer-installer": "^0.5 || This Composer plugin will sort out the PHPCS 'installed_paths' automatically.", 102 | "roave/security-advisories": "dev-master || Helps prevent installing dependencies with known security issues." 103 | }, 104 | "type": "phpcodesniffer-standard", 105 | "notification-url": "https://packagist.org/downloads/", 106 | "license": [ 107 | "LGPL-3.0-or-later" 108 | ], 109 | "authors": [ 110 | { 111 | "name": "Wim Godden", 112 | "homepage": "https://github.com/wimg", 113 | "role": "lead" 114 | }, 115 | { 116 | "name": "Juliette Reinders Folmer", 117 | "homepage": "https://github.com/jrfnl", 118 | "role": "lead" 119 | }, 120 | { 121 | "name": "Contributors", 122 | "homepage": "https://github.com/PHPCompatibility/PHPCompatibility/graphs/contributors" 123 | } 124 | ], 125 | "description": "A set of sniffs for PHP_CodeSniffer that checks for PHP cross-version compatibility.", 126 | "homepage": "http://techblog.wimgodden.be/tag/codesniffer/", 127 | "keywords": [ 128 | "compatibility", 129 | "phpcs", 130 | "standards" 131 | ], 132 | "time": "2019-12-27T09:44:58+00:00" 133 | }, 134 | { 135 | "name": "phpcompatibility/phpcompatibility-paragonie", 136 | "version": "1.3.0", 137 | "source": { 138 | "type": "git", 139 | "url": "https://github.com/PHPCompatibility/PHPCompatibilityParagonie.git", 140 | "reference": "b862bc32f7e860d0b164b199bd995e690b4b191c" 141 | }, 142 | "dist": { 143 | "type": "zip", 144 | "url": "https://api.github.com/repos/PHPCompatibility/PHPCompatibilityParagonie/zipball/b862bc32f7e860d0b164b199bd995e690b4b191c", 145 | "reference": "b862bc32f7e860d0b164b199bd995e690b4b191c", 146 | "shasum": "" 147 | }, 148 | "require": { 149 | "phpcompatibility/php-compatibility": "^9.0" 150 | }, 151 | "require-dev": { 152 | "dealerdirect/phpcodesniffer-composer-installer": "^0.5", 153 | "paragonie/random_compat": "dev-master", 154 | "paragonie/sodium_compat": "dev-master" 155 | }, 156 | "suggest": { 157 | "dealerdirect/phpcodesniffer-composer-installer": "^0.5 || This Composer plugin will sort out the PHP_CodeSniffer 'installed_paths' automatically.", 158 | "roave/security-advisories": "dev-master || Helps prevent installing dependencies with known security issues." 159 | }, 160 | "type": "phpcodesniffer-standard", 161 | "notification-url": "https://packagist.org/downloads/", 162 | "license": [ 163 | "LGPL-3.0-or-later" 164 | ], 165 | "authors": [ 166 | { 167 | "name": "Wim Godden", 168 | "role": "lead" 169 | }, 170 | { 171 | "name": "Juliette Reinders Folmer", 172 | "role": "lead" 173 | } 174 | ], 175 | "description": "A set of rulesets for PHP_CodeSniffer to check for PHP cross-version compatibility issues in projects, while accounting for polyfills provided by the Paragonie polyfill libraries.", 176 | "homepage": "http://phpcompatibility.com/", 177 | "keywords": [ 178 | "compatibility", 179 | "paragonie", 180 | "phpcs", 181 | "polyfill", 182 | "standards" 183 | ], 184 | "time": "2019-11-04T15:17:54+00:00" 185 | }, 186 | { 187 | "name": "phpcompatibility/phpcompatibility-wp", 188 | "version": "2.1.0", 189 | "source": { 190 | "type": "git", 191 | "url": "https://github.com/PHPCompatibility/PHPCompatibilityWP.git", 192 | "reference": "41bef18ba688af638b7310666db28e1ea9158b2f" 193 | }, 194 | "dist": { 195 | "type": "zip", 196 | "url": "https://api.github.com/repos/PHPCompatibility/PHPCompatibilityWP/zipball/41bef18ba688af638b7310666db28e1ea9158b2f", 197 | "reference": "41bef18ba688af638b7310666db28e1ea9158b2f", 198 | "shasum": "" 199 | }, 200 | "require": { 201 | "phpcompatibility/php-compatibility": "^9.0", 202 | "phpcompatibility/phpcompatibility-paragonie": "^1.0" 203 | }, 204 | "require-dev": { 205 | "dealerdirect/phpcodesniffer-composer-installer": "^0.5" 206 | }, 207 | "suggest": { 208 | "dealerdirect/phpcodesniffer-composer-installer": "^0.5 || This Composer plugin will sort out the PHP_CodeSniffer 'installed_paths' automatically.", 209 | "roave/security-advisories": "dev-master || Helps prevent installing dependencies with known security issues." 210 | }, 211 | "type": "phpcodesniffer-standard", 212 | "notification-url": "https://packagist.org/downloads/", 213 | "license": [ 214 | "LGPL-3.0-or-later" 215 | ], 216 | "authors": [ 217 | { 218 | "name": "Wim Godden", 219 | "role": "lead" 220 | }, 221 | { 222 | "name": "Juliette Reinders Folmer", 223 | "role": "lead" 224 | } 225 | ], 226 | "description": "A ruleset for PHP_CodeSniffer to check for PHP cross-version compatibility issues in projects, while accounting for polyfills provided by WordPress.", 227 | "homepage": "http://phpcompatibility.com/", 228 | "keywords": [ 229 | "compatibility", 230 | "phpcs", 231 | "standards", 232 | "wordpress" 233 | ], 234 | "time": "2019-08-28T14:22:28+00:00" 235 | }, 236 | { 237 | "name": "squizlabs/php_codesniffer", 238 | "version": "3.5.3", 239 | "source": { 240 | "type": "git", 241 | "url": "https://github.com/squizlabs/PHP_CodeSniffer.git", 242 | "reference": "557a1fc7ac702c66b0bbfe16ab3d55839ef724cb" 243 | }, 244 | "dist": { 245 | "type": "zip", 246 | "url": "https://api.github.com/repos/squizlabs/PHP_CodeSniffer/zipball/557a1fc7ac702c66b0bbfe16ab3d55839ef724cb", 247 | "reference": "557a1fc7ac702c66b0bbfe16ab3d55839ef724cb", 248 | "shasum": "" 249 | }, 250 | "require": { 251 | "ext-simplexml": "*", 252 | "ext-tokenizer": "*", 253 | "ext-xmlwriter": "*", 254 | "php": ">=5.4.0" 255 | }, 256 | "require-dev": { 257 | "phpunit/phpunit": "^4.0 || ^5.0 || ^6.0 || ^7.0" 258 | }, 259 | "bin": [ 260 | "bin/phpcs", 261 | "bin/phpcbf" 262 | ], 263 | "type": "library", 264 | "extra": { 265 | "branch-alias": { 266 | "dev-master": "3.x-dev" 267 | } 268 | }, 269 | "notification-url": "https://packagist.org/downloads/", 270 | "license": [ 271 | "BSD-3-Clause" 272 | ], 273 | "authors": [ 274 | { 275 | "name": "Greg Sherwood", 276 | "role": "lead" 277 | } 278 | ], 279 | "description": "PHP_CodeSniffer tokenizes PHP, JavaScript and CSS files and detects violations of a defined set of coding standards.", 280 | "homepage": "https://github.com/squizlabs/PHP_CodeSniffer", 281 | "keywords": [ 282 | "phpcs", 283 | "standards" 284 | ], 285 | "time": "2019-12-04T04:46:47+00:00" 286 | }, 287 | { 288 | "name": "wp-coding-standards/wpcs", 289 | "version": "2.2.0", 290 | "source": { 291 | "type": "git", 292 | "url": "https://github.com/WordPress/WordPress-Coding-Standards.git", 293 | "reference": "f90e8692ce97b693633db7ab20bfa78d930f536a" 294 | }, 295 | "dist": { 296 | "type": "zip", 297 | "url": "https://api.github.com/repos/WordPress/WordPress-Coding-Standards/zipball/f90e8692ce97b693633db7ab20bfa78d930f536a", 298 | "reference": "f90e8692ce97b693633db7ab20bfa78d930f536a", 299 | "shasum": "" 300 | }, 301 | "require": { 302 | "php": ">=5.4", 303 | "squizlabs/php_codesniffer": "^3.3.1" 304 | }, 305 | "require-dev": { 306 | "dealerdirect/phpcodesniffer-composer-installer": "^0.5.0", 307 | "phpcompatibility/php-compatibility": "^9.0", 308 | "phpunit/phpunit": "^4.0 || ^5.0 || ^6.0 || ^7.0" 309 | }, 310 | "suggest": { 311 | "dealerdirect/phpcodesniffer-composer-installer": "^0.5.0 || This Composer plugin will sort out the PHPCS 'installed_paths' automatically." 312 | }, 313 | "type": "phpcodesniffer-standard", 314 | "notification-url": "https://packagist.org/downloads/", 315 | "license": [ 316 | "MIT" 317 | ], 318 | "authors": [ 319 | { 320 | "name": "Contributors", 321 | "homepage": "https://github.com/WordPress/WordPress-Coding-Standards/graphs/contributors" 322 | } 323 | ], 324 | "description": "PHP_CodeSniffer rules (sniffs) to enforce WordPress coding conventions", 325 | "keywords": [ 326 | "phpcs", 327 | "standards", 328 | "wordpress" 329 | ], 330 | "time": "2019-11-11T12:34:03+00:00" 331 | } 332 | ], 333 | "aliases": [], 334 | "minimum-stability": "stable", 335 | "stability-flags": [], 336 | "prefer-stable": false, 337 | "prefer-lowest": false, 338 | "platform": { 339 | "php": ">=5.6" 340 | }, 341 | "platform-dev": [], 342 | "platform-overrides": { 343 | "php": "5.6" 344 | } 345 | } 346 | -------------------------------------------------------------------------------- /guidepost.php: -------------------------------------------------------------------------------- 1 | array(), 28 | 'version' => GUIDEPOST_VERSION, 29 | ); 30 | 31 | // Editor Script. 32 | $asset_filepath = GUIDEPOST_PLUGIN_DIR . '/build/index.asset.php'; 33 | $asset_file = file_exists( $asset_filepath ) ? include $asset_filepath : $default_asset_file; 34 | 35 | wp_register_script( 36 | 'guidepost-editor', 37 | GUIDEPOST_PLUGIN_URL . 'build/index.js', 38 | $asset_file['dependencies'], 39 | $asset_file['version'], 40 | true // Enqueue script in the footer. 41 | ); 42 | 43 | // Editor Styles. 44 | $asset_filepath = GUIDEPOST_PLUGIN_DIR . '/build/guidepost-editor.asset.php'; 45 | $asset_file = file_exists( $asset_filepath ) ? include $asset_filepath : $default_asset_file; 46 | 47 | wp_register_style( 48 | 'guidepost-editor', 49 | GUIDEPOST_PLUGIN_URL . 'build/guidepost-editor.css', 50 | array(), 51 | $asset_file['version'] 52 | ); 53 | 54 | // Frontend Styles. 55 | $asset_filepath = GUIDEPOST_PLUGIN_DIR . '/build/guidepost-style.asset.php'; 56 | $asset_file = file_exists( $asset_filepath ) ? include $asset_filepath : $default_asset_file; 57 | 58 | wp_register_style( 59 | 'guidepost-frontend', 60 | GUIDEPOST_PLUGIN_URL . 'build/guidepost-style.css', 61 | array(), 62 | $asset_file['version'] 63 | ); 64 | 65 | register_block_type( 66 | 'sortabrilliant/guidepost', 67 | array( 68 | 'editor_script' => 'guidepost-editor', 69 | 'editor_style' => 'guidepost-editor', 70 | 'style' => 'guidepost-frontend', 71 | ) 72 | ); 73 | } 74 | 75 | add_action( 'init', 'guidepost_register_block' ); 76 | 77 | /** 78 | * Enqueues the frontend script. 79 | */ 80 | function guidepost_enqueue_scripts() { 81 | $asset_filepath = GUIDEPOST_PLUGIN_DIR . '/build/guidepost-theme.asset.php'; 82 | $asset_file = file_exists( $asset_filepath ) ? include $asset_filepath : array( 83 | 'dependencies' => array(), 84 | 'version' => GUIDEPOST_VERSION, 85 | ); 86 | 87 | if ( has_block( 'sortabrilliant/guidepost' ) ) { 88 | wp_enqueue_script( 89 | 'guidepost-frontend', 90 | GUIDEPOST_PLUGIN_URL . 'build/guidepost-theme.js', 91 | $asset_file['dependencies'], 92 | $asset_file['version'], 93 | true 94 | ); 95 | } 96 | } 97 | 98 | add_action( 'wp_enqueue_scripts', 'guidepost_enqueue_scripts' ); 99 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "scripts": { 4 | "start": "wp-scripts start", 5 | "build": "wp-scripts build", 6 | "env": "wp-scripts env", 7 | "lint-js": "wp-scripts lint-js", 8 | "lint-php": "wp-scripts env lint-php" 9 | }, 10 | "devDependencies": { 11 | "@wordpress/scripts": "^6.2.0", 12 | "css-loader": "^3.4.2", 13 | "mini-css-extract-plugin": "^0.9.0", 14 | "node-sass": "^4.13.1", 15 | "sass-loader": "^8.0.2", 16 | "webpack-fix-style-only-entries": "^0.4.0" 17 | }, 18 | "wp-env": { 19 | "plugin-dir": "guidepost", 20 | "plugin-name": "Guidepost", 21 | "welcome-build-command": "npm start" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /phpcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Rules for sortabrilliant 4 | 5 | 6 | 7 | ./ 8 | 9 | */build/* 10 | */vendor/* 11 | */node_modules/* 12 | */wordpress/* 13 | */\.* 14 | 15 | 16 | 17 | 18 | 19 | 20 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | -------------------------------------------------------------------------------- /readme.txt: -------------------------------------------------------------------------------- 1 | === Guidepost === 2 | Contributors: sortabrilliant, jrtashjian 3 | Tags: heading, style, block 4 | Requires at least: 5.0 5 | Tested up to: 5.4.0 6 | Stable tag: 1.2.1 7 | Requires PHP: 5.6 8 | License: GPLv2 or later 9 | License URI: https://www.gnu.org/licenses/gpl-2.0.html 10 | 11 | A guidepost gives you directions. It lets you know where you’re going. It gives you a preview of what’s to come. 12 | 13 | == Description == 14 | A guidepost gives you directions. It lets you know where you’re going. It gives you a preview of what’s to come. 15 | 16 | #### How does it work? 17 | 18 | Guideposts are magic, no they really are. Whenever you add or remove a heading the guidepost block is automatically updated. 19 | 20 | Nesting is done based on which size heading you use. Headings of smaller sizes are automatically nested under ones of larger sizes. 21 | 22 | #### I like this, who made it? 23 | This plugin is brought to you by [sorta brilliant](https://sortabrilliant.com/) and [block garden](https://block.garden). 24 | 25 | == Screenshots == 26 | 1. Add the Guidepost block and everything is taken care of automatically. 27 | 28 | == Changelog == 29 | 30 | ### Bug Fixes 31 | * Remove extraneous HTML elements in saved output 32 | * Fix visual nesting of elements when editing 33 | 34 | ### Misc 35 | * Bumping "Tested up to" for WordPress 5.4.0 36 | -------------------------------------------------------------------------------- /src/Guidepost.js: -------------------------------------------------------------------------------- 1 | 2 | export const Guidepost = ( props ) => ( 3 | 14 | ); 15 | 16 | export const Node = ( props ) => ( 17 |
  • 18 | 19 | { props.content } 20 | 21 | { props.children && ( 22 | 23 | ) } 24 |
  • 25 | ); 26 | -------------------------------------------------------------------------------- /src/block.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sortabrilliant/guidepost", 3 | "title": "Guidepost", 4 | "category": "common", 5 | "description": "Add a list of internal links allowing your readers to quickly navigate around.", 6 | "keywords": [ "toc", "sortabrilliant" ], 7 | "textDomain": "guidepost", 8 | "attributes": { 9 | "headings": { 10 | "source": "query", 11 | "selector": "a", 12 | "query": { 13 | "content": { 14 | "source": "text" 15 | }, 16 | "anchor": { 17 | "source": "attribute", 18 | "attribute": "href" 19 | }, 20 | "level": { 21 | "source": "attribute", 22 | "attribute": "data-level" 23 | } 24 | } 25 | } 26 | }, 27 | "editorScript": "build/index.js", 28 | "editorStyle": "build/guidepost-editor.css", 29 | "style": "build/guidepost-style.css" 30 | } -------------------------------------------------------------------------------- /src/deprecated.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Internal dependencies 3 | */ 4 | import metadata from './block.json'; 5 | import { linearToNestedList } from './linear-to-nested-list'; 6 | 7 | /** 8 | * WordPress dependencies 9 | */ 10 | import { Disabled } from '@wordpress/components'; 11 | 12 | const Guidepost = ( props ) => ( 13 | 24 | ); 25 | 26 | const Node = ( props ) => ( 27 |
  • 28 | 29 | 30 | { props.content } 31 | 32 | 33 | { props.children && ( 34 | 35 | ) } 36 |
  • 37 | ); 38 | 39 | export const deprecated = [ 40 | { 41 | attributes: metadata.attributes, 42 | save: ( { attributes, className } ) => { 43 | const { headings = [] } = attributes; 44 | 45 | return ( headings < 1 ) ? null : ( 46 |
    47 | 48 |
    49 | ); 50 | }, 51 | }, 52 | ]; 53 | -------------------------------------------------------------------------------- /src/edit.js: -------------------------------------------------------------------------------- 1 | /** 2 | * External dependencies 3 | */ 4 | import { kebabCase, isEqual } from 'lodash'; 5 | 6 | /** 7 | * Internal dependencies 8 | */ 9 | import { Guidepost } from './Guidepost'; 10 | import { Icon as icon } from './icon'; 11 | import { linearToNestedList } from './linear-to-nested-list'; 12 | 13 | /** 14 | * WordPress dependencies 15 | */ 16 | import { Component } from '@wordpress/element'; 17 | import { Placeholder, Disabled } from '@wordpress/components'; 18 | import { __ } from '@wordpress/i18n'; 19 | import { compose } from '@wordpress/compose'; 20 | import { withSelect, withDispatch } from '@wordpress/data'; 21 | 22 | class GuidepostEdit extends Component { 23 | componentDidUpdate( prevProps ) { 24 | const { 25 | headingBlocks, 26 | headings, 27 | setAttributes, 28 | updateBlockAttributes, 29 | } = this.props; 30 | 31 | if ( ! isEqual( prevProps.attributes.headings, headings ) ) { 32 | // Generate new anchors for duplicated headings. 33 | headingBlocks.forEach( ( heading, index ) => { 34 | const duplicateHeadings = headingBlocks.filter( ( block ) => block.attributes.anchor === heading.attributes.anchor ); 35 | 36 | if ( ( duplicateHeadings && duplicateHeadings.length > 1 ) || typeof heading.attributes.anchor === 'undefined' || heading.attributes.anchor === '' ) { 37 | updateBlockAttributes( 38 | heading.clientId, 39 | { anchor: index + '-' + kebabCase( heading.attributes.content ) } 40 | ); 41 | } 42 | } ); 43 | 44 | setAttributes( { headings } ); 45 | } 46 | } 47 | 48 | render() { 49 | const { headings = [] } = this.props; 50 | 51 | if ( headings < 1 ) { 52 | return ( 53 | 58 | ); 59 | } 60 | 61 | const nestedHeadings = linearToNestedList( headings ); 62 | 63 | return ( 64 |
    65 | 66 | 67 | 68 |
    69 | ); 70 | } 71 | } 72 | 73 | export const Edit = compose( [ 74 | withSelect( ( select ) => { 75 | const { getBlocks } = select( 'core/block-editor' ); 76 | 77 | const headingBlocks = getBlocks().filter( ( block ) => block.name === 'core/heading' && !! block.attributes.content ); 78 | 79 | return { 80 | headingBlocks, 81 | headings: headingBlocks.map( ( block ) => ( { 82 | content: block.attributes.content, 83 | anchor: '#' + block.attributes.anchor, 84 | level: block.attributes.level.toString(), 85 | } ) ), 86 | }; 87 | } ), 88 | withDispatch( ( dispatch ) => { 89 | const { updateBlockAttributes } = dispatch( 'core/block-editor' ); 90 | 91 | return { 92 | updateBlockAttributes, 93 | }; 94 | } ), 95 | ] )( GuidepostEdit ); 96 | -------------------------------------------------------------------------------- /src/guidepost-theme.js: -------------------------------------------------------------------------------- 1 | ( function() { 2 | 'use strict'; 3 | 4 | window.addEventListener( 'DOMContentLoaded', () => { 5 | const bttTarget = 'guidepost'; 6 | let bttVisible = false; 7 | 8 | const guidepostBlock = document.querySelector( '.wp-block-sortabrilliant-guidepost' ); 9 | 10 | if ( typeof guidepostBlock !== 'undefined' ) { 11 | const backToTopElement = document.createElement( 'a' ); 12 | backToTopElement.className = 'sortabrilliant-guidepost-button'; 13 | backToTopElement.href = `#${ bttTarget }`; 14 | 15 | backToTopElement.innerHTML = ''; 16 | 17 | guidepostBlock.id = bttTarget; 18 | guidepostBlock.appendChild( backToTopElement ); 19 | 20 | window.addEventListener( 'scroll', function() { 21 | const guidepostOffsetTop = guidepostBlock.offsetTop; 22 | if ( window.scrollY > guidepostOffsetTop && ! bttVisible ) { 23 | bttVisible = true; 24 | backToTopElement.classList.add( 'visible' ); 25 | } else if ( window.scrollY < guidepostOffsetTop && bttVisible ) { 26 | bttVisible = false; 27 | backToTopElement.classList.remove( 'visible' ); 28 | } 29 | } ); 30 | 31 | [ ...document.getElementsByTagName( 'a' ) ] 32 | .filter( ( element ) => !! element.hash ) 33 | .forEach( ( element ) => { 34 | const scrollToTarget = ( targetElementId ) => { 35 | const scrollToElement = document.getElementById( 36 | targetElementId.split( '#' )[ 1 ] 37 | ); 38 | 39 | const easeInCubic = ( t ) => t * t * t; 40 | 41 | let start = null; 42 | const duration = 500; 43 | 44 | const animationStep = ( timestamp ) => { 45 | start = start || timestamp; 46 | const runtime = timestamp - start; 47 | 48 | const ease = easeInCubic( runtime / duration ); 49 | window.scroll( 0, window.pageYOffset + ( scrollToElement.getBoundingClientRect().top * ease ) ); 50 | 51 | if ( runtime < duration ) { 52 | requestAnimationFrame( animationStep ); 53 | } 54 | }; 55 | 56 | requestAnimationFrame( animationStep ); 57 | }; 58 | 59 | element.addEventListener( 'click', ( event ) => { 60 | event.preventDefault(); 61 | scrollToTarget( event.currentTarget.hash ); 62 | } ); 63 | } ); 64 | } 65 | } ); 66 | }() ); 67 | -------------------------------------------------------------------------------- /src/icon.js: -------------------------------------------------------------------------------- 1 | /** 2 | * WordPress dependencies 3 | */ 4 | import { Path, SVG } from '@wordpress/components'; 5 | 6 | export const Icon = ; 7 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Internal dependencies 3 | */ 4 | import metadata from './block.json'; 5 | import { deprecated } from './deprecated'; 6 | import { Edit as edit } from './edit'; 7 | import { Icon as icon } from './icon'; 8 | import { Save as save } from './save'; 9 | 10 | /** 11 | * WordPress dependencies 12 | */ 13 | import { __ } from '@wordpress/i18n'; 14 | import { registerBlockType, createBlock } from '@wordpress/blocks'; 15 | 16 | registerBlockType( 'sortabrilliant/guidepost', { 17 | title: __( 'Guidepost', 'guidepost' ), 18 | description: __( 'Add a list of internal links allowing your readers to quickly navigate around.', 'guidepost' ), 19 | icon, 20 | category: metadata.category, 21 | keywords: [ 22 | __( 'toc', 'guidepost' ), 23 | __( 'sortabrilliant', 'guidepost' ), 24 | ], 25 | 26 | attributes: metadata.attributes, 27 | 28 | supports: { 29 | html: false, 30 | multiple: false, 31 | }, 32 | 33 | transforms: { 34 | from: [ 35 | { 36 | type: 'block', 37 | blocks: [ '*' ], 38 | isMatch: ( { originalName } ) => originalName === 'sbb/guidepost', 39 | transform: () => createBlock( 'sortabrilliant/guidepost' ), 40 | }, 41 | ], 42 | }, 43 | 44 | deprecated, 45 | edit, 46 | save, 47 | } ); 48 | -------------------------------------------------------------------------------- /src/linear-to-nested-list.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Convert a linear list of core/heading blocks into a nested list based on heading levels. 3 | * 4 | * @param {Array} array List of core/heading blocks. 5 | * @return {Array} Nested list of core/heading blocks. 6 | */ 7 | export function linearToNestedList( array ) { 8 | const returnValue = []; 9 | 10 | array.forEach( function( heading, key ) { 11 | if ( typeof heading.content === 'undefined' ) { 12 | return; 13 | } 14 | 15 | // Make sure we are only working with the same level as the first iteration in our set. 16 | if ( heading.level === array[ 0 ].level ) { 17 | // Check that the next iteration will return a value. 18 | // If it does and the next level is greater than the current level, 19 | // the next iteration becomes a child of the current interation. 20 | if ( 21 | ( typeof array[ key + 1 ] !== 'undefined' ) && 22 | ( array[ key + 1 ].level > heading.level ) 23 | ) { 24 | // We need to calculate the last index before the next iteration that has the same level (siblings). 25 | // We then use this last index to slice the array for use in recursion. 26 | // This prevents duplicate nodes. 27 | let endOfSlice = array.length; 28 | for ( let i = ( key + 1 ); i < array.length; i++ ) { 29 | if ( array[ i ].level === heading.level ) { 30 | endOfSlice = i; 31 | break; 32 | } 33 | } 34 | 35 | // We found a child node: Push a new node onto the return array with children. 36 | returnValue.push( { 37 | block: heading, 38 | children: linearToNestedList( array.slice( key + 1, endOfSlice ) ), 39 | } ); 40 | } else { 41 | // No child node: Push a new node onto the return array. 42 | returnValue.push( { 43 | block: heading, 44 | children: null, 45 | } ); 46 | } 47 | } 48 | } ); 49 | 50 | return returnValue; 51 | } 52 | -------------------------------------------------------------------------------- /src/save.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Internal dependencies 3 | */ 4 | import { Guidepost } from './Guidepost'; 5 | import { linearToNestedList } from './linear-to-nested-list'; 6 | 7 | /** 8 | * WordPress dependencies 9 | */ 10 | import { Component } from '@wordpress/element'; 11 | 12 | export class Save extends Component { 13 | render() { 14 | const { headings = [] } = this.props.attributes; 15 | 16 | return ( headings < 1 ) ? null : ( 17 |
    18 | 19 |
    20 | ); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/styles/editor.scss: -------------------------------------------------------------------------------- 1 | .wp-block[data-type="sortabrilliant/guidepost"] { 2 | ul { 3 | padding-left: 1.3em; 4 | margin-left: 1.3em; 5 | } 6 | } -------------------------------------------------------------------------------- /src/styles/style.scss: -------------------------------------------------------------------------------- 1 | .wp-block-sortabrilliant-guidepost { 2 | .sortabrilliant-guidepost-button { 3 | display: flex; 4 | padding: 17px; 5 | 6 | position: fixed; 7 | bottom: 30px; 8 | right: 30px; 9 | z-index: 999999; 10 | 11 | opacity: 0; 12 | 13 | background-color: rgba( 170, 170, 170, .5 ); 14 | transition: background-color .3s, opacity .3s; 15 | 16 | &, &:hover { 17 | box-shadow: none; 18 | } 19 | 20 | &:hover { 21 | background-color: rgba( 170, 170, 170, 1 ); 22 | } 23 | 24 | svg { 25 | position: relative; 26 | top: 3px; 27 | height: 26px; 28 | width: 26px; 29 | } 30 | 31 | &.visible { 32 | opacity: 1; 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require( 'path' ); 2 | const defaultConfig = require( '@wordpress/scripts/config/webpack.config' ); 3 | 4 | const MiniCssExtractPlugin = require( 'mini-css-extract-plugin' ); 5 | const FixStyleOnlyEntriesPlugin = require( 'webpack-fix-style-only-entries' ); 6 | 7 | const isProduction = process.env.NODE_ENV === 'production'; 8 | 9 | module.exports = { 10 | ...defaultConfig, 11 | 12 | entry: { 13 | ...defaultConfig.entry, 14 | 15 | 'guidepost-editor': path.resolve( process.cwd(), 'src/styles/editor.scss' ), 16 | 'guidepost-style': path.resolve( process.cwd(), 'src/styles/style.scss' ), 17 | 18 | 'guidepost-theme': path.resolve( process.cwd(), 'src/guidepost-theme.js' ), 19 | }, 20 | 21 | module: { 22 | ...defaultConfig.module, 23 | rules: [ 24 | ...defaultConfig.module.rules, 25 | { 26 | test: /\.scss$/, 27 | use: [ 28 | MiniCssExtractPlugin.loader, 29 | { loader: 'css-loader', options: { url: false, sourceMap: ! isProduction } }, 30 | { loader: 'sass-loader', options: { sourceMap: ! isProduction } }, 31 | ], 32 | }, 33 | ], 34 | }, 35 | 36 | plugins: [ 37 | ...defaultConfig.plugins, 38 | new FixStyleOnlyEntriesPlugin(), 39 | new MiniCssExtractPlugin( { filename: '[name].css' } ), 40 | ], 41 | }; 42 | --------------------------------------------------------------------------------