├── .dev-lib
├── .eslintrc
├── .gitignore
├── .gitmodules
├── .jscsrc
├── .jshintrc
├── .travis.yml
├── Gruntfile.js
├── customize-controls.js
├── customizer-browser-history.php
├── frontend-scroll-persistence.js
├── package.json
├── phpcs.xml
├── readme.md
├── readme.txt
└── wp-assets
├── banner-1544x500.png
├── banner-772x250.png
├── banner.svg
├── icon-128x128.png
├── icon-256x256.png
└── icon.svg
/.dev-lib:
--------------------------------------------------------------------------------
1 | WPCS_GIT_TREE=develop
2 | ASSETS_DIR=wp-assets
3 |
--------------------------------------------------------------------------------
/.eslintrc:
--------------------------------------------------------------------------------
1 | dev-lib/.eslintrc
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | build
3 |
--------------------------------------------------------------------------------
/.gitmodules:
--------------------------------------------------------------------------------
1 | [submodule "dev-lib"]
2 | path = dev-lib
3 | url = https://github.com/xwp/wp-dev-lib.git
4 | branch = master
5 |
--------------------------------------------------------------------------------
/.jscsrc:
--------------------------------------------------------------------------------
1 | dev-lib/.jscsrc
--------------------------------------------------------------------------------
/.jshintrc:
--------------------------------------------------------------------------------
1 | dev-lib/.jshintrc
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | sudo: false
2 | dist: precise
3 |
4 | language:
5 | - php
6 | - node_js
7 |
8 | php:
9 | - 5.6
10 |
11 | env:
12 | - WP_VERSION=4.7 WP_MULTISITE=0
13 | - WP_VERSION=4.7 WP_MULTISITE=1
14 | - WP_VERSION=trunk WP_MULTISITE=0
15 | - WP_VERSION=trunk WP_MULTISITE=1
16 |
17 | install:
18 | - export DEV_LIB_PATH=dev-lib
19 | - if [ ! -e "$DEV_LIB_PATH" ] && [ -L .travis.yml ]; then export DEV_LIB_PATH=$( dirname $( readlink .travis.yml ) ); fi
20 | - if [ ! -e "$DEV_LIB_PATH" ]; then git clone https://github.com/xwp/wp-dev-lib.git $DEV_LIB_PATH; fi
21 | - source $DEV_LIB_PATH/travis.install.sh
22 |
23 | script:
24 | - source $DEV_LIB_PATH/travis.script.sh
25 |
26 | after_script:
27 | - source $DEV_LIB_PATH/travis.after_script.sh
28 |
--------------------------------------------------------------------------------
/Gruntfile.js:
--------------------------------------------------------------------------------
1 | /* eslint-env node */
2 | /* jshint node:true */
3 |
4 | module.exports = function( grunt ) {
5 | 'use strict';
6 |
7 | grunt.initConfig( {
8 |
9 | pkg: grunt.file.readJSON( 'package.json' ),
10 |
11 | // JavaScript linting with JSHint.
12 | jshint: {
13 | options: {
14 | jshintrc: '.jshintrc'
15 | },
16 | all: [
17 | 'Gruntfile.js',
18 | '*.js'
19 | ]
20 | },
21 |
22 | // Build a deploy-able plugin
23 | copy: {
24 | build: {
25 | src: [
26 | '*.php',
27 | '*.js',
28 | 'readme.txt',
29 | '!Gruntfile.js'
30 | ],
31 | dest: 'build',
32 | expand: true,
33 | dot: true
34 | }
35 | },
36 |
37 | // Clean up the build
38 | clean: {
39 | build: {
40 | src: [ 'build' ]
41 | }
42 | },
43 |
44 | // VVV (Varying Vagrant Vagrants) Paths
45 | vvv: {
46 | 'plugin': '/srv/www/wordpress-develop/src/wp-content/plugins/<%= pkg.name %>',
47 | 'coverage': '/srv/www/default/coverage/<%= pkg.name %>'
48 | },
49 |
50 | // Shell actions
51 | shell: {
52 | options: {
53 | stdout: true,
54 | stderr: true
55 | },
56 | readme: {
57 | command: 'cd ./dev-lib && ./generate-markdown-readme' // Generate the readme.md
58 | },
59 | phpunit: {
60 | command: 'vagrant ssh -c "cd <%= vvv.plugin %> && phpunit"'
61 | },
62 | phpunit_c: {
63 | command: 'vagrant ssh -c "cd <%= vvv.plugin %> && phpunit --coverage-html <%= vvv.coverage %>"'
64 | }
65 | },
66 |
67 | // Deploys a git Repo to the WordPress SVN repo
68 | wp_deploy: {
69 | deploy: {
70 | options: {
71 | plugin_slug: '<%= pkg.name %>',
72 | build_dir: 'build',
73 | assets_dir: 'wp-assets'
74 | }
75 | }
76 | }
77 |
78 | } );
79 |
80 | // Load tasks
81 | grunt.loadNpmTasks( 'grunt-contrib-clean' );
82 | grunt.loadNpmTasks( 'grunt-contrib-copy' );
83 | grunt.loadNpmTasks( 'grunt-contrib-jshint' );
84 | grunt.loadNpmTasks( 'grunt-shell' );
85 | grunt.loadNpmTasks( 'grunt-wp-deploy' );
86 |
87 | // Register tasks
88 | grunt.registerTask( 'default', [
89 | 'jshint'
90 | ] );
91 |
92 | grunt.registerTask( 'readme', [
93 | 'shell:readme'
94 | ] );
95 |
96 | grunt.registerTask( 'phpunit', [
97 | 'shell:phpunit'
98 | ] );
99 |
100 | grunt.registerTask( 'phpunit_c', [
101 | 'shell:phpunit_c'
102 | ] );
103 |
104 | grunt.registerTask( 'dev', [
105 | 'default',
106 | 'readme'
107 | ] );
108 |
109 | grunt.registerTask( 'build', [
110 | 'default',
111 | 'readme',
112 | 'copy'
113 | ] );
114 |
115 | grunt.registerTask( 'deploy', [
116 | 'build',
117 | 'wp_deploy',
118 | 'clean'
119 | ] );
120 |
121 | };
122 |
--------------------------------------------------------------------------------
/customize-controls.js:
--------------------------------------------------------------------------------
1 | /* global wp */
2 | /* exported CustomizerBrowserHistory */
3 | /* eslint no-magic-numbers: ["error", { "ignore": [-1,0,1,2,10] }] */
4 | /* eslint complexity: ["error", 8] */
5 |
6 | var CustomizerBrowserHistory = (function( api, $ ) {
7 | 'use strict';
8 |
9 | var component = {
10 | defaultQueryParamValues: {},
11 | previousQueryParams: {},
12 | expandedPanel: new api.Value(),
13 | expandedSection: new api.Value(),
14 | expandedOuterSection: new api.Value(),
15 | expandedControl: new api.Value(),
16 | previewScrollPosition: new api.Value( 0 )
17 | };
18 |
19 | /**
20 | * Get current query params.
21 | *
22 | * @param {string} url URL.
23 | * @returns {object} Query params.
24 | */
25 | component.getQueryParams = function getQueryParams( url ) {
26 | var urlParser, queryParams, queryString;
27 | urlParser = document.createElement( 'a' );
28 | urlParser.href = url;
29 | queryParams = {};
30 |
31 | queryString = urlParser.search.substr( 1 );
32 | if ( queryString ) {
33 | queryParams = api.utils.parseQueryString( queryString );
34 | }
35 |
36 | // Cast scroll to integer.
37 | if ( ! _.isUndefined( queryParams.scroll ) ) {
38 | queryParams.scroll = parseInt( queryParams.scroll, 10 );
39 | if ( isNaN( queryParams.scroll ) ) {
40 | delete queryParams.scroll;
41 | }
42 | }
43 |
44 | return queryParams;
45 | };
46 |
47 | /**
48 | * Update the URL state with the current Customizer state, using pushState for url changes and replaceState for other changes.
49 | *
50 | * @returns {void}
51 | */
52 | component.updateWindowLocation = _.debounce( function updateWindowLocation() { // eslint-disable-line complexity
53 | var expandedPanel = '', expandedSection = '', expandedControl = '', values, urlParser,
54 | oldQueryParams, newQueryParams, setQueryParams, urlChanged = false, expandedOuterSection = '';
55 |
56 | api.panel.each( function( panel ) {
57 | if ( panel.active() && panel.expanded() ) {
58 | expandedPanel = panel.id;
59 | }
60 | } );
61 | api.section.each( function( section ) {
62 | if ( section.active() && section.expanded() ) {
63 | if ( api.OuterSection && section.extended( api.OuterSection ) ) {
64 | expandedOuterSection = section.id;
65 | } else {
66 | expandedSection = section.id;
67 | }
68 | }
69 | } );
70 | if ( expandedSection ) {
71 | api.control.each( function( control ) {
72 | if ( expandedSection && control.section() === expandedSection && control.active() && control.expanded && control.expanded() ) {
73 | expandedControl = control.id;
74 | }
75 | } );
76 | }
77 |
78 | component.expandedPanel.set( expandedPanel );
79 | component.expandedSection.set( expandedSection );
80 | component.expandedOuterSection.set( expandedOuterSection );
81 | component.expandedControl.set( expandedControl );
82 | component.previewScrollPosition.set( api.previewer.scroll );
83 |
84 | oldQueryParams = component.getQueryParams( location.href );
85 | newQueryParams = {};
86 | values = {
87 | 'url': api.previewer.previewUrl,
88 | 'autofocus[panel]': component.expandedPanel,
89 | 'autofocus[section]': component.expandedSection,
90 | 'autofocus[outer_section]': component.expandedOuterSection,
91 | 'autofocus[control]': component.expandedControl,
92 | 'device': api.previewedDevice,
93 | 'scroll': component.previewScrollPosition
94 | };
95 |
96 | // Preserve extra vars.
97 | _.each( _.keys( oldQueryParams ), function( key ) {
98 | if ( 'undefined' === typeof values[ key ] ) {
99 | newQueryParams[ key ] = oldQueryParams[ key ];
100 | }
101 | } );
102 |
103 | // Collect new query params.
104 | _.each( values, function( valueObj, key ) {
105 | var value = valueObj.get();
106 | if ( null !== value ) {
107 | newQueryParams[ key ] = value;
108 | }
109 | } );
110 |
111 | /*
112 | * There can only be one. Well, there should be. Let presence control override section,
113 | * and section override panel. But if something was not registered in PHP, then include
114 | * the autofocus parameter for its parent as it is likely lazy-loaded upon parent expanded.
115 | */
116 | if ( newQueryParams['autofocus[section]'] && ! _.isUndefined( api.settings.sections[ newQueryParams['autofocus[section]'] ] ) ) {
117 | delete newQueryParams['autofocus[panel]'];
118 | }
119 | if ( newQueryParams['autofocus[control]'] && ! _.isUndefined( api.settings.sections[ newQueryParams['autofocus[control]'] ] ) ) {
120 | delete newQueryParams['autofocus[section]'];
121 | }
122 |
123 | // Delete the section if its parent panel is not expanded.
124 | if (
125 | component.expandedSection.get() &&
126 | api.section.has( component.expandedSection.get() ) &&
127 | api.section( component.expandedSection.get() ).panel() &&
128 | api.panel.has( api.section( component.expandedSection.get() ).panel() ) &&
129 | ! api.panel( api.section( component.expandedSection.get() ).panel() ).expanded()
130 | ) {
131 | delete newQueryParams['autofocus[section]'];
132 | }
133 |
134 | if ( ! _.isEqual( newQueryParams, oldQueryParams ) ) {
135 | setQueryParams = {};
136 | _.each( newQueryParams, function( value, key ) {
137 | if ( value !== component.defaultQueryParamValues[ key ] ) {
138 | setQueryParams[ key ] = value;
139 | }
140 | } );
141 |
142 | urlParser = document.createElement( 'a' );
143 | urlParser.href = location.href;
144 | urlParser.search = _.map( setQueryParams, function( value, key ) {
145 | var pair = encodeURIComponent( key );
146 | if ( null !== value ) {
147 | pair += '=' + encodeURIComponent( value );
148 | }
149 | pair = pair.replace( /%5B/g, '[' ).replace( /%5D/g, ']' ).replace( /%2F/g, '/' ).replace( /%3A/g, ':' );
150 | return pair;
151 | } ).join( '&' );
152 |
153 | if ( newQueryParams.url !== ( oldQueryParams.url || component.defaultQueryParamValues.url ) ) {
154 | urlChanged = true;
155 | }
156 |
157 | // Send the state to the parent window.
158 | if ( urlChanged ) {
159 | history.pushState( {}, '', urlParser.href );
160 | } else {
161 | history.replaceState( {}, '', urlParser.href );
162 | }
163 | component.previousQueryParams = newQueryParams;
164 | }
165 | } );
166 |
167 | /**
168 | * On history popstate, set the URL to match.
169 | *
170 | * @param {object} queryParams Query params.
171 | * @returns {void}
172 | */
173 | component.updatePreviewUrl = function updatePreviewUrl( queryParams ) {
174 | var url = null;
175 |
176 | // Preserve the old scroll position.
177 | if ( queryParams.scroll ) {
178 | api.previewer.scroll = queryParams.scroll;
179 | } else {
180 | api.previewer.scroll = 0;
181 | }
182 |
183 | // Update the url.
184 | if ( queryParams.url ) {
185 | url = queryParams.url;
186 | } else {
187 | url = api.settings.url.preview; // On pop to initial state, the state is null.
188 | }
189 | api.previewer.previewUrl.set( url );
190 | };
191 |
192 | /**
193 | * Watch for changes to a construct's active and expanded states.
194 | *
195 | * @param {wp.customize.Panel|wp.customize.Section|wp.customize.Control} construct Construct.
196 | * @returns {void}
197 | */
198 | component.watchExpandedChange = function watchExpandedChange( construct ) {
199 | if ( construct.active ) {
200 | construct.active.bind( component.updateWindowLocation );
201 | }
202 | if ( construct.expanded ) {
203 | construct.expanded.bind( component.updateWindowLocation );
204 | }
205 | component.updateWindowLocation();
206 | };
207 |
208 | /**
209 | * Unwatch for changes to a construct's active and expanded states.
210 | *
211 | * @param {wp.customize.Panel|wp.customize.Section|wp.customize.Control} construct Construct.
212 | * @returns {void}
213 | */
214 | component.unwatchExpandedChange = function unwatchExpandedChange( construct ) {
215 | if ( construct.active ) {
216 | construct.active.unbind( component.updateWindowLocation );
217 | }
218 | if ( construct.expanded ) {
219 | construct.expanded.unbind( component.updateWindowLocation );
220 | }
221 |
222 | // Because 'remove' event is triggered before the construct is removed. See #37269.
223 | _.delay( function() {
224 | component.updateWindowLocation();
225 | } );
226 | };
227 |
228 | /*
229 | * Strip out autofocus[section]=installed_themes and autofocus[panel]=themes before redirection.
230 | * The user will not want to see the theme browser after switching a theme.
231 | */
232 | if ( api.ThemesPanel ) {
233 | api.ThemesPanel.prototype.loadThemePreview = (function( loadThemePreview ) {
234 | return function( themeId ) {
235 | var panel = this, promise, urlReplaced = false, queryParams, urlParser, originalUrl = location.href; // eslint-disable-line consistent-this
236 |
237 | // Strip autofocus params from URL when they are for themes panel.
238 | if ( panel.expanded() ) {
239 | queryParams = component.getQueryParams( originalUrl );
240 | delete queryParams['autofocus[panel]'];
241 | delete queryParams['autofocus[control]'];
242 | delete queryParams['autofocus[section]'];
243 | urlParser = document.createElement( 'a' );
244 | urlParser.href = location.href;
245 | urlParser.search = $.param( queryParams );
246 | if ( urlParser.href !== location.href ) {
247 | history.replaceState( {}, '', urlParser.href );
248 | urlReplaced = true;
249 | }
250 | }
251 |
252 | promise = loadThemePreview.call( panel, themeId );
253 |
254 | // Restore the original URL when loading of theme fails.
255 | promise.fail( function() {
256 | if ( urlReplaced ) {
257 | history.replaceState( {}, '', originalUrl );
258 | }
259 | } );
260 | return promise;
261 | };
262 | })( api.ThemesPanel.prototype.loadThemePreview );
263 | }
264 |
265 | /**
266 | * Update window.location to sync with customizer state.
267 | *
268 | * @returns {void}
269 | */
270 | component.startUpdatingWindowLocation = function startUpdatingWindowLocation() {
271 | var outerSection, outerSectionParam = 'autofocus[outer_section]',
272 | currentQueryParams = component.getQueryParams( location.href );
273 |
274 | if ( currentQueryParams.scroll ) {
275 | component.previewScrollPosition.set( currentQueryParams.scroll );
276 | api.previewer.scroll = component.previewScrollPosition.get();
277 | api.previewer.send( 'scroll', component.previewScrollPosition.get() );
278 | }
279 |
280 | if ( _.has( currentQueryParams, outerSectionParam ) && api.section.has( currentQueryParams[ outerSectionParam ] ) ) {
281 | outerSection = api.section( currentQueryParams[ outerSectionParam ] );
282 | outerSection.deferred.embedded.done( function() {
283 | outerSection.focus();
284 | } );
285 | }
286 |
287 | component.defaultQueryParamValues = {
288 | 'device': (function() {
289 | var defaultPreviewedDevice = null;
290 | _.find( api.settings.previewableDevices, function checkDefaultPreviewedDevice( params, device ) {
291 | if ( true === params['default'] ) {
292 | defaultPreviewedDevice = device;
293 | return true;
294 | }
295 | return false;
296 | } );
297 | return defaultPreviewedDevice;
298 | } )(),
299 | 'scroll': 0,
300 | 'url': api.settings.url.home,
301 | 'autofocus[panel]': '',
302 | 'autofocus[section]': '',
303 | 'autofocus[outer_section]': '',
304 | 'autofocus[control]': ''
305 | };
306 |
307 | component.previousQueryParams = _.extend( {}, currentQueryParams );
308 |
309 | $( window ).on( 'popstate', function onPopState( event ) {
310 | var urlParser, queryParams;
311 | urlParser = document.createElement( 'a' );
312 | urlParser.href = location.href;
313 | queryParams = api.utils.parseQueryString( urlParser.search.substr( 1 ) );
314 |
315 | component.updatePreviewUrl( queryParams );
316 |
317 | // Make sure the current changeset_uuid is in the URL (if changesets are available).
318 | if ( api.settings.changeset && queryParams.changeset_uuid !== api.settings.changeset.uuid ) {
319 | queryParams.changeset_uuid = api.settings.changeset.uuid;
320 | urlParser.search = $.param( queryParams ).replace( /%5B/g, '[' ).replace( /%5D/g, ']' ).replace( /%2F/g, '/' ).replace( /%3A/g, ':' );
321 | history.replaceState( event.originalEvent.state, '', urlParser.href );
322 | }
323 | } );
324 |
325 | component.expandedPanel.set( api.settings.autofocus.panel || '' );
326 | component.expandedSection.set( api.settings.autofocus.section || '' );
327 | component.expandedControl.set( api.settings.autofocus.control || '' );
328 |
329 | api.control.each( component.watchExpandedChange );
330 | api.section.each( component.watchExpandedChange );
331 | api.panel.each( component.watchExpandedChange );
332 |
333 | api.control.bind( 'add', component.watchExpandedChange );
334 | api.section.bind( 'add', component.watchExpandedChange );
335 | api.panel.bind( 'add', component.watchExpandedChange );
336 |
337 | api.control.bind( 'remove', component.unwatchExpandedChange );
338 | api.section.bind( 'remove', component.unwatchExpandedChange );
339 | api.panel.bind( 'remove', component.unwatchExpandedChange );
340 |
341 | api.previewedDevice.bind( component.updateWindowLocation );
342 | api.previewer.previewUrl.bind( component.updateWindowLocation );
343 | api.previewer.bind( 'scroll', component.updateWindowLocation );
344 | component.previewScrollPosition.bind( component.updateWindowLocation );
345 |
346 | component.updateWindowLocation();
347 | };
348 |
349 | /**
350 | * Set up functionality.
351 | *
352 | * @returns {void}
353 | */
354 | component.ready = function ready() {
355 | var closeLink = $( '.customize-controls-close' ), rememberScrollPosition;
356 |
357 | // Short-circuit if not supported or if using customize-loader.
358 | if ( ! history.replaceState || ! history.pushState || top !== window ) {
359 | return;
360 | }
361 |
362 | rememberScrollPosition = function() {
363 | sessionStorage.setItem( 'lastCustomizerScrollPosition', api.previewer.scroll );
364 | };
365 |
366 | // Update close link URL to be preview URL and remember scroll position if close link is not back to WP Admin.
367 | if ( -1 === closeLink.prop( 'pathname' ).indexOf( '/wp-admin/' ) ) {
368 |
369 | // Sync the preview URL to the close button so the URL navigated to upon closing is the last URL which was previewed.
370 | api.previewer.previewUrl.bind( function( newPreviewUrl ) {
371 | var urlParser = document.createElement( 'a' );
372 | urlParser.href = newPreviewUrl;
373 |
374 | // Only copy the path and query vars since the frontend URL may have a different domain.
375 | closeLink.prop( 'pathname', urlParser.pathname );
376 | closeLink.prop( 'search', urlParser.search );
377 | });
378 |
379 | // Remember scroll position if we're going back to the frontend.
380 | closeLink.on( 'click', rememberScrollPosition );
381 | }
382 |
383 | /*
384 | * Start syncing state once the preview loads so that the active panels/sections/controls
385 | * have been set to prevent the URL from being momentarily having autofocus params removed.
386 | */
387 | api.previewer.deferred.active.done( component.startUpdatingWindowLocation );
388 | };
389 |
390 | /**
391 | * Initialize component.
392 | *
393 | * @returns {void}
394 | */
395 | component.init = function init() {
396 | api.bind( 'ready', component.ready );
397 | };
398 |
399 | return component;
400 | })( wp.customize, jQuery );
401 |
--------------------------------------------------------------------------------
/customizer-browser-history.php:
--------------------------------------------------------------------------------
1 | #28536 and it works best with the Customize Snapshots plugin.
6 | * Plugin URI: https://github.com/xwp/wp-customizer-browser-history
7 | * Author: Weston Ruter, XWP
8 | * Author URI: https://make.xwp.co/
9 | * Domain Path: /languages
10 | *
11 | * Copyright (c) 2016 XWP (https://make.xwp.co/)
12 | *
13 | * This program is free software; you can redistribute it and/or modify
14 | * it under the terms of the GNU General Public License, version 2 or, at
15 | * your discretion, any later version, as published by the Free
16 | * Software Foundation.
17 | *
18 | * This program is distributed in the hope that it will be useful,
19 | * but WITHOUT ANY WARRANTY; without even the implied warranty of
20 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21 | * GNU General Public License for more details.
22 | *
23 | * You should have received a copy of the GNU General Public License
24 | * along with this program; if not, write to the Free Software
25 | * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
26 | *
27 | * @package CustomizerBrowserHistory
28 | */
29 |
30 | define( 'CUSTOMIZER_BROWSER_HISTORY_VERSION', '0.5.2' );
31 |
32 | /**
33 | * Register and enqueue customizer controls scripts.
34 | */
35 | function customizer_browser_history_enqueue_controls_scripts() {
36 | $handle = 'customizer-browser-history-customize-controls';
37 | $src = plugin_dir_url( __FILE__ ) . 'customize-controls.js';
38 | $deps = array( 'customize-controls' );
39 | $ver = CUSTOMIZER_BROWSER_HISTORY_VERSION;
40 | wp_enqueue_script( $handle, $src, $deps, $ver );
41 |
42 | wp_add_inline_script( $handle, 'CustomizerBrowserHistory.init();' );
43 | }
44 | add_action( 'customize_controls_enqueue_scripts', 'customizer_browser_history_enqueue_controls_scripts' );
45 |
46 | /**
47 | * Register and enqueue non-preview frontend scripts.
48 | */
49 | function customizer_browser_history_enqueue_frontend_scripts() {
50 | if ( ! is_user_logged_in() || ! is_admin_bar_showing() || ! current_user_can( 'customize' ) ) {
51 | return;
52 | }
53 |
54 | $handle = '/customizer-browser-history-frontend-scroll-persistence';
55 | $src = plugin_dir_url( __FILE__ ) . 'frontend-scroll-persistence.js';
56 | $deps = array( 'jquery', 'underscore' );
57 | $ver = CUSTOMIZER_BROWSER_HISTORY_VERSION;
58 | wp_enqueue_script( $handle, $src, $deps, $ver );
59 |
60 | wp_add_inline_script( $handle, 'CustomizerBrowserHistoryFrontendScrollPersistence.init();' );
61 | }
62 | add_action( 'wp_enqueue_scripts', 'customizer_browser_history_enqueue_frontend_scripts' );
63 |
64 | /**
65 | * Filter the available devices to change default based on device query param.
66 | *
67 | * @see WP_Customize_Manager::get_previewable_devices()
68 | * @param array $devices List of devices with labels and default setting.
69 | * @return array Devices.
70 | */
71 | function customizer_browser_history_filter_default_previewable_devices( $devices ) {
72 | if ( ! isset( $_GET['device'] ) ) {
73 | return $devices;
74 | }
75 | $device_name = wp_unslash( $_GET['device'] );
76 | if ( ! isset( $devices[ $device_name ] ) ) {
77 | return $devices;
78 | }
79 | foreach ( $devices as &$device ) {
80 | unset( $device['default'] );
81 | }
82 | $devices[ $device_name ]['default'] = true;
83 | return $devices;
84 | }
85 | add_filter( 'customize_previewable_devices', 'customizer_browser_history_filter_default_previewable_devices' );
86 |
--------------------------------------------------------------------------------
/frontend-scroll-persistence.js:
--------------------------------------------------------------------------------
1 | /* global _ */
2 | /* eslint no-magic-numbers: ["error", { "ignore": [0,1] }] */
3 | /* exported CustomizerBrowserHistoryFrontendScrollPersistence */
4 |
5 | var CustomizerBrowserHistoryFrontendScrollPersistence = (function( $ ) {
6 | 'use strict';
7 |
8 | var component = {};
9 |
10 | /**
11 | * Update scroll param.
12 | *
13 | * @returns {void}
14 | */
15 | component.updateScrollParam = function updateScrollParam() {
16 | var query = $( this ).prop( 'search' ).substr( 1 );
17 |
18 | // Remove existing scroll param.
19 | query = _.filter( query.split( /&/ ), function( pair ) {
20 | return ! /^scroll=/.test( pair );
21 | }).join( '&' );
22 |
23 | // Append new scroll param to query string.
24 | if ( query.length > 0 ) {
25 | query += '&';
26 | }
27 | query += 'scroll=' + String( $( window ).scrollTop() );
28 |
29 | // Update query string.
30 | $( this ).prop( 'search', query );
31 | };
32 |
33 | /**
34 | * Restore the scroll position the user was last at in the Customizer preview.
35 | *
36 | * @returns {void}
37 | */
38 | component.restoreScrollPosition = function restoreScrollPosition() {
39 | if ( 'undefined' !== typeof sessionStorage && sessionStorage.getItem( 'lastCustomizerScrollPosition' ) ) {
40 | $( window ).scrollTop( parseInt( sessionStorage.getItem( 'lastCustomizerScrollPosition' ), 10 ) );
41 | sessionStorage.removeItem( 'lastCustomizerScrollPosition' );
42 | }
43 | };
44 |
45 | /**
46 | * Set up functionality.
47 | *
48 | * @returns {void}
49 | */
50 | component.ready = function ready() {
51 | component.restoreScrollPosition();
52 |
53 | $( '#wp-admin-bar-customize > a' ).on( 'mouseover mousedown click', component.updateScrollParam );
54 | };
55 |
56 | /**
57 | * Initialize.
58 | *
59 | * @returns {void}
60 | */
61 | component.init = function init() {
62 | $( component.ready );
63 | };
64 |
65 | return component;
66 | })( jQuery );
67 |
68 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "customizer-browser-history",
3 | "title": "Customizer Browser History",
4 | "version": "0.5.2",
5 | "scripts": {
6 | "test": "echo \"Error: no test specified\" && exit 1"
7 | },
8 | "repository": {
9 | "type": "git",
10 | "url": "git+https://github.com/xwp/wp-customizer-browser-history.git"
11 | },
12 | "author": "XWP",
13 | "license": "GPL-2.0",
14 | "bugs": {
15 | "url": "https://github.com/xwp/wp-customizer-browser-history/issues"
16 | },
17 | "homepage": "https://github.com/xwp/wp-customizer-browser-history#readme",
18 | "devDependencies": {
19 | "eslint": "^3.13.1",
20 | "grunt": "~0.4.5",
21 | "grunt-contrib-clean": "~1.0.0",
22 | "grunt-contrib-copy": "~1.0.0",
23 | "grunt-contrib-jshint": "~1.0.0",
24 | "grunt-shell": "^1.3.1",
25 | "grunt-wp-deploy": "^1.2.1"
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/phpcs.xml:
--------------------------------------------------------------------------------
1 |
2 |
29 | autofocus[panel]=nav_menus&autofocus[section]=nav_menu[87]&autofocus[control]=nav_menu_item[5123] 30 |31 | 32 | And while these changes to the `autofocus` params are being made in the browser's URL as the Customizer UI is interacted with, if you navigate to another page in the preview the `url` parameter will also be replaced to reflect the new preview URL. 33 | 34 | Note that the `url` param will be URL-encoded. So a typical Customizer URL would get updated to look like: 35 | 36 |
37 | http://example.com/wp-admin/customize.php?url=http%3A%2F%2Fexample.com%2Fsample-page%2F&autofocus[panel]=widgets&autofocus[section]=sidebar-widgets-sidebar-1&autofocus[control]=widget_text[10]&device=mobile&scroll=200 38 |39 | 40 | The plugin will also persist the scroll position from the frontend to preview frame in the Customizer after clicking the “Customize” link in the frontend admin bar. This ensures you can quickly start editing whatever you were looking at the moment you clicked Customize, and it makes the Customizer load from the frontend in a more seamless way. 41 | 42 | Additionally, as you navigate around the Customizer preview, the close link in the Customizer controls pane will keep updating to point to the same URL that you are previewing, along with persisting the scroll position. In this way, whenever you close the Customizer via this link the user experience is that the Customizer sidebar is just removed, similar to as if they clicked the “Hide Controls” link at the bottom of the sidebar. This behavior is only active if the user had clicked the Customize link from the frontend. If they clicked Customize from the admin, then the Close link will remain linking back to the admin page they came from. Note that for responsive themes like Twenty Seventeen, the synced scroll position between the frontend and backend won't always appear seamless since the Customizer controls panel being expanded causes the element dimensions in the preview to change. 43 | 44 | **Development of this plugin is done [on GitHub](https://github.com/xwp/wp-customizer-browser-history). Pull requests welcome. Please see [issues](https://github.com/xwp/wp-customizer-browser-history/issues) reported there before going to the [plugin forum](https://wordpress.org/support/plugin/customizer-browser-history).** 45 | 46 | ## Changelog ## 47 | 48 | ### 0.5.2 ### 49 | * Prevent autofocus params for themes panel and its sections from being included during theme switch. See [#22](https://github.com/xwp/wp-customizer-browser-history/issues/22). 50 | * Only remove parent construct autofocus params if child is defined statically; ensures that lazy-loaded children can be autofocused. 51 | * Remove code now irrelevant as of WordPress 4.7. 52 | * Bump minimum WordPress requirement to 4.7. 53 | * Bump tested to 4.9. 54 | 55 | ### 0.5.1 ### 56 | Fix reference to `package.json` which is not included in build. 57 | 58 | ### 0.5.0 ### 59 | Persist scroll position and previewed URL between frontend and customizer preview. See [#20](https://github.com/xwp/wp-customizer-browser-history/pull/20). 60 | 61 | ### 0.4.7 ### 62 | Prevent dropping non-home initial `url` param when loading customizer. See [#19](https://github.com/xwp/wp-customizer-browser-history/pull/19). 63 | 64 | ### 0.4.6 ### 65 | Fix compatibility with WordPress 4.6. See [#17](https://github.com/xwp/wp-customizer-browser-history/pull/17). 66 | 67 | ### 0.4.5 ### 68 | Ensure `changeset_uuid` param is added to `customize.php` URL if state is dirty OR the changeset post exists. PR [#16](https://github.com/xwp/wp-customizer-browser-history/pull/16). 69 | 70 | ### 0.4.4 ### 71 | Only include one `autofocus` param. If `autofocus[control]` is present, skip including `autofocus[section]` (since implied). Likewise, if `autofocus[section]` is present, also exclude its containing `autofocus[panel]` since it is also implied. By only including one `autofocus` param the URL bar is less cluttered, but also an issue is fixed where focus may not reliably be added due to apparent inconsistencies in which construct is autofocused first (the control should really be the last to get focus). 72 | 73 | ### 0.4.3 ### 74 | Send scroll message to previewer to fix 4.7 scroll position. 75 | 76 | ### 0.4.2 ### 77 | Misc cleanup and improve integration with WP 4.7. 78 | 79 | ### 0.4.1 ### 80 | Fixed issue whereby an expanded widget control could persist its `autofocus` param when another section is expanded. 81 | 82 | ### 0.4.0 ### 83 | * Added persistence of `scroll` position when navigating back/forward in the preview and when reloading the customizer. 84 | * Renamed the `customize_previewed_device` query param to just `device`. 85 | * Improved the building of the URL query params to omit any params that are the same as the defaults, so `device=desktop` and `scroll=0` should not be shown, nor should a `url` that points to the home URL. 86 | * Fixed dropping of value-less query params, e.g. `customize.php?debug` 87 | 88 | ### 0.3.0 ### 89 | * Add back/forward browser history for navigation in the Customizer preview. See [#2](https://github.com/xwp/wp-customizer-browser-history/issues/2), PR [#8](https://github.com/xwp/wp-customizer-browser-history/pull/8). 90 | * Eliminate initial insertion of `url` and `customize_previewed_device` params when same as default. 91 | 92 | ### 0.2.0 ### 93 | Persist the device being previewed (desktop, tablet, mobile) in the URL via a new `customize_previewed_device` query param. See [#3](https://github.com/xwp/wp-customizer-browser-history/issues/3). 94 | 95 | ### 0.1.1 ### 96 | Remove `autofocus[control]` when there is not a section expanded, such as when a widget is expanded when the sidebar section is collapsed. 97 | 98 | ### 0.1.0 ### 99 | Initial release. 100 | 101 | 102 | -------------------------------------------------------------------------------- /readme.txt: -------------------------------------------------------------------------------- 1 | === Customizer Browser History === 2 | Contributors: xwp, westonruter 3 | Tags: customizer, customize 4 | Requires at least: 4.7 5 | Tested up to: 4.9 6 | Stable tag: 0.5.2 7 | License: GPLv2 or later 8 | License URI: http://www.gnu.org/licenses/gpl-2.0.html 9 | 10 | Sync browser URL in Customizer with current preview URL and focused panels, sections, and controls. 11 | 12 | == Description == 13 | 14 | *This is a feature plugin intended to implement [#28536](https://core.trac.wordpress.org/ticket/28536): Add browser history and deep linking for navigation in Customizer preview* 15 | 16 | This plugin keeps the Customizer URL in the browser updated with current previewed URL as the `url` query param and current expanded panel/section/control as `autofocus` params. This allows for bookmarking and also the ability to reload and return go the same view (which is great for developers), including which device you are previewing (desktop, tablet, or mobile). Not only will the URL be kept in sync with the current customizer UI, but new browser history entries will be added as you navigate around the site in the preview (via `history.pushState()`), allowing you to use the back/forward buttons as you would normally when browsing the site outside the customizer. The scroll position for each previewed URL is tracked as well, so that when you navigate back/forward the scroll position will be restored, just as happens when browsing the site outside the customizer preview. Restoring the scroll position also works when reloading the customizer, as the position is persisted in a `scroll` query parameter: again, this is extremely useful during development. 17 | 18 | This plugin complements well the Customize Snapshots plugin which allows you to save your Customizer state in a shapshot/changeset with an associated UUID that also gets added to the browser URL in the Customizer. 19 | 20 | For example, if you load the Customizer and then click the “Site Identity” section, the URL will be replaced to add `autofocus[section]=title_tagline`. 21 | 22 | If you navigate into the nav menus panel, open a menu section, and then expand a nav menu item control, then the URL will have these `autofocus` params added: 23 | 24 |
25 | autofocus[panel]=nav_menus&autofocus[section]=nav_menu[87]&autofocus[control]=nav_menu_item[5123] 26 |27 | 28 | And while these changes to the `autofocus` params are being made in the browser's URL as the Customizer UI is interacted with, if you navigate to another page in the preview the `url` parameter will also be replaced to reflect the new preview URL. 29 | 30 | Note that the `url` param will be URL-encoded. So a typical Customizer URL would get updated to look like: 31 | 32 |
33 | http://example.com/wp-admin/customize.php?url=http%3A%2F%2Fexample.com%2Fsample-page%2F&autofocus[panel]=widgets&autofocus[section]=sidebar-widgets-sidebar-1&autofocus[control]=widget_text[10]&device=mobile&scroll=200 34 |35 | 36 | The plugin will also persist the scroll position from the frontend to preview frame in the Customizer after clicking the “Customize” link in the frontend admin bar. This ensures you can quickly start editing whatever you were looking at the moment you clicked Customize, and it makes the Customizer load from the frontend in a more seamless way. 37 | 38 | Additionally, as you navigate around the Customizer preview, the close link in the Customizer controls pane will keep updating to point to the same URL that you are previewing, along with persisting the scroll position. In this way, whenever you close the Customizer via this link the user experience is that the Customizer sidebar is just removed, similar to as if they clicked the “Hide Controls” link at the bottom of the sidebar. This behavior is only active if the user had clicked the Customize link from the frontend. If they clicked Customize from the admin, then the Close link will remain linking back to the admin page they came from. Note that for responsive themes like Twenty Seventeen, the synced scroll position between the frontend and backend won't always appear seamless since the Customizer controls panel being expanded causes the element dimensions in the preview to change. 39 | 40 | **Development of this plugin is done [on GitHub](https://github.com/xwp/wp-customizer-browser-history). Pull requests welcome. Please see [issues](https://github.com/xwp/wp-customizer-browser-history/issues) reported there before going to the [plugin forum](https://wordpress.org/support/plugin/customizer-browser-history).** 41 | 42 | == Changelog == 43 | 44 | = 0.5.2 = 45 | 46 | * Prevent autofocus params for themes panel and its sections from being included during theme switch. See [#22](https://github.com/xwp/wp-customizer-browser-history/issues/22). 47 | * Only remove parent construct autofocus params if child is defined statically; ensures that lazy-loaded children can be autofocused. 48 | * Remove code now irrelevant as of WordPress 4.7. 49 | * Bump minimum WordPress requirement to 4.7. 50 | * Bump tested to 4.9. 51 | 52 | = 0.5.1 = 53 | 54 | Fix reference to `package.json` which is not included in build. 55 | 56 | = 0.5.0 = 57 | 58 | Persist scroll position and previewed URL between frontend and customizer preview. See [#20](https://github.com/xwp/wp-customizer-browser-history/pull/20). 59 | 60 | = 0.4.7 = 61 | 62 | Prevent dropping non-home initial `url` param when loading customizer. See [#19](https://github.com/xwp/wp-customizer-browser-history/pull/19). 63 | 64 | = 0.4.6 = 65 | 66 | Fix compatibility with WordPress 4.6. See [#17](https://github.com/xwp/wp-customizer-browser-history/pull/17). 67 | 68 | = 0.4.5 = 69 | 70 | Ensure `changeset_uuid` param is added to `customize.php` URL if state is dirty OR the changeset post exists. PR [#16](https://github.com/xwp/wp-customizer-browser-history/pull/16). 71 | 72 | = 0.4.4 = 73 | 74 | Only include one `autofocus` param. If `autofocus[control]` is present, skip including `autofocus[section]` (since implied). Likewise, if `autofocus[section]` is present, also exclude its containing `autofocus[panel]` since it is also implied. By only including one `autofocus` param the URL bar is less cluttered, but also an issue is fixed where focus may not reliably be added due to apparent inconsistencies in which construct is autofocused first (the control should really be the last to get focus). 75 | 76 | = 0.4.3 = 77 | 78 | Send scroll message to previewer to fix 4.7 scroll position. 79 | 80 | = 0.4.2 = 81 | 82 | Misc cleanup and improve integration with WP 4.7. 83 | 84 | = 0.4.1 = 85 | 86 | Fixed issue whereby an expanded widget control could persist its `autofocus` param when another section is expanded. 87 | 88 | = 0.4.0 = 89 | 90 | * Added persistence of `scroll` position when navigating back/forward in the preview and when reloading the customizer. 91 | * Renamed the `customize_previewed_device` query param to just `device`. 92 | * Improved the building of the URL query params to omit any params that are the same as the defaults, so `device=desktop` and `scroll=0` should not be shown, nor should a `url` that points to the home URL. 93 | * Fixed dropping of value-less query params, e.g. `customize.php?debug` 94 | 95 | = 0.3.0 = 96 | 97 | * Add back/forward browser history for navigation in the Customizer preview. See [#2](https://github.com/xwp/wp-customizer-browser-history/issues/2), PR [#8](https://github.com/xwp/wp-customizer-browser-history/pull/8). 98 | * Eliminate initial insertion of `url` and `customize_previewed_device` params when same as default. 99 | 100 | = 0.2.0 = 101 | 102 | Persist the device being previewed (desktop, tablet, mobile) in the URL via a new `customize_previewed_device` query param. See [#3](https://github.com/xwp/wp-customizer-browser-history/issues/3). 103 | 104 | = 0.1.1 = 105 | 106 | Remove `autofocus[control]` when there is not a section expanded, such as when a widget is expanded when the sidebar section is collapsed. 107 | 108 | = 0.1.0 = 109 | 110 | Initial release. 111 | -------------------------------------------------------------------------------- /wp-assets/banner-1544x500.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xwp/wp-customizer-browser-history/6c96fb0ab8d38eb649588c7b313b8e072bfa0f85/wp-assets/banner-1544x500.png -------------------------------------------------------------------------------- /wp-assets/banner-772x250.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xwp/wp-customizer-browser-history/6c96fb0ab8d38eb649588c7b313b8e072bfa0f85/wp-assets/banner-772x250.png -------------------------------------------------------------------------------- /wp-assets/banner.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /wp-assets/icon-128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xwp/wp-customizer-browser-history/6c96fb0ab8d38eb649588c7b313b8e072bfa0f85/wp-assets/icon-128x128.png -------------------------------------------------------------------------------- /wp-assets/icon-256x256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xwp/wp-customizer-browser-history/6c96fb0ab8d38eb649588c7b313b8e072bfa0f85/wp-assets/icon-256x256.png -------------------------------------------------------------------------------- /wp-assets/icon.svg: -------------------------------------------------------------------------------- 1 | --------------------------------------------------------------------------------