首页 / 中文教程 / 教程

Bricks 构建器自定义键盘快捷键:单键插入元素 + Alt+H 切换 :hover

这篇文章提供一段代码,给 Bricks 编辑器配置几个自定义键盘快捷键,让建站工作流更快一些。

Ray Chan·2026-08-12·约 10 分钟

这篇文章提供一段代码,给 Bricks 编辑器配置几个自定义键盘快捷键,让建站工作流更快一些。

按键 动作
s 插入区块(Section),并自动激活其内部的容器(Container)
c 插入容器(Container)
b 插入(Block)
d 插入 Div
t 插入文本(Basic Text)
h 插入标题(Heading)
i 插入图片(Image)
r 插入富文本(Rich Text)
l 插入文本链接(Text Link)
w 用配置的包裹元素包裹选中的元素(默认:Block)
Alt+H 切换 :hover 伪类

有了这些快捷键,在 Bricks 里搭建页面会非常快。

另外,这段代码还把 Bricks 的默认行为改了:结构面板里原本单击即可重命名元素(个人认为这个交互很糟糕),改为双击才可重命名。

注意:

  • 单键快捷键(s、c、b、d、t、h、i、r、l、w)只在没有按下任何修饰键时生效。
  • 在表单输入框、CodeMirror 编辑器里打字时,或命令面板(command palette)打开时,所有快捷键都会禁用。
  • 快捷键在构建器主面板和画布 iframe 里都有效。
  • w 键遵循 Bricks 的 builderWrapElement 设置(与 CMD/CTRL+Shift+P 相同)。

第一步:修改 functions.php

编辑子主题functions.php

把:

add_action( 'wp_enqueue_scripts', function() {
	// Enqueue your files on the canvas & frontend, not the builder panel. Otherwise custom CSS might affect builder)
	if ( ! bricks_is_builder_main() ) {
		wp_enqueue_style( 'bricks-child', get_stylesheet_uri(), ['bricks-frontend'], filemtime( get_stylesheet_directory() . '/style.css' ) );
	}
} );

替换为:

add_action( 'wp_enqueue_scripts', function() {
	// Enqueue your files on the canvas & frontend, not the builder panel. Otherwise custom CSS might affect builder)
	if ( ! bricks_is_builder_main() ) {
		wp_enqueue_style( 'bricks-child', get_stylesheet_uri(), ['bricks-frontend'], filemtime( get_stylesheet_directory() . '/style.css' ) );
	}
	
	// Enqueue builder keyboard shortcuts (Alt+H to toggle :hover, single key shortcuts)
	if ( bricks_is_builder_main() ) {
		wp_enqueue_script(
			'bl-builder-shortcuts',
			get_stylesheet_directory_uri() . '/js/builder-shortcuts.js',
			array( 'bricks-builder' ),
			filemtime( get_stylesheet_directory() . '/js/builder-shortcuts.js' ),
			true,
		);
	}
} );

第二步:创建 builder-shortcuts.js

在子主题目录里新建一个名为 js 的文件夹。

js 文件夹里创建文件 builder-shortcuts.js,代码如下:

( function () {
	/**
	 * Single-key shortcuts to insert elements.
	 *
	 * s = Section, c = Container, b = Block, d = Div,
	 * t = Text (Basic), h = Heading, i = Image,
	 * r = Rich Text, l = Text Link,
	 * w = Wrap selected element(s) with configured wrap element
	 */
	var ELEMENT_SHORTCUTS = {
		KeyS: 'section',
		KeyC: 'container',
		KeyB: 'block',
		KeyD: 'div',
		KeyT: 'text-basic',
		KeyH: 'heading',
		KeyI: 'image',
		KeyR: 'text',
		KeyL: 'text-link',
	};

	/* -----------------------------------------------------------------------
	 * Vue reference caching (lazy-init).
	 *
	 * The Bricks Vue app, its globalProperties and $_state are stable
	 * references for the entire builder session. We cache them on first
	 * access instead of querying the DOM on every keypress.
	 * ----------------------------------------------------------------------- */
	var _gp = null;
	var _state = null;

	/**
	 * Return the cached Vue global properties, initialising on first call.
	 */
	function gp() {
		if ( ! _gp ) {
			var app = window.top.document.querySelector( '.brx-body' );

			if ( app && app.__vue_app__ ) {
				_gp = app.__vue_app__.config.globalProperties;
				_state = _gp.$_state;
			}
		}

		return _gp;
	}

	/* -----------------------------------------------------------------------
	 * Element lookup helper.
	 *
	 * Prefers Bricks' built-in $_getDynamicElementById (likely O(1)) and
	 * falls back to an array search on $_dynamicElements.value.
	 * ----------------------------------------------------------------------- */

	/**
	 * Look up an element by its ID using the fastest available method.
	 *
	 * @param {string} id Element ID.
	 * @return {Object|null} The element object or null.
	 */
	function getElementById( id ) {
		var props = gp();

		if ( ! props ) {
			return null;
		}

		// Bricks provides $_getDynamicElementById for fast lookups.
		if ( typeof props.$_getDynamicElementById === 'function' ) {
			return props.$_getDynamicElementById( id );
		}

		// Fallback: linear search through the elements array.
		var elements = props.$_dynamicElements && props.$_dynamicElements.value;

		return elements
			? elements.find( function ( el ) { return el.id === id; } )
			: null;
	}

	/* -----------------------------------------------------------------------
	 * Editable-target guard.
	 * ----------------------------------------------------------------------- */

	/**
	 * Check if the event target is an editable field where typing should be allowed.
	 * Readonly inputs inside the structure panel are NOT considered editable (the user
	 * is just selecting an element, not renaming it).
	 */
	function isEditableTarget( e ) {
		var tag = e.target.tagName;

		if ( tag === 'INPUT' ) {
			// Allow shortcuts on readonly structure-panel label inputs.
			if ( e.target.readOnly && e.target.closest( '.structure-item .title' ) ) {
				return false;
			}

			return true;
		}

		if ( tag === 'TEXTAREA' || e.target.isContentEditable ) {
			return true;
		}

		// CodeMirror editor
		if ( e.target.closest && e.target.closest( '.CodeMirror' ) ) {
			return true;
		}

		return false;
	}

	/* -----------------------------------------------------------------------
	 * Shortcut handlers — action-only (guard logic lives in the dispatcher).
	 * ----------------------------------------------------------------------- */

	/**
	 * Toggle :hover pseudo-class.
	 */
	function toggleHover( e ) {
		e.preventDefault();

		if ( ! _state ) {
			return;
		}

		_state.pseudoClassActive = _state.pseudoClassActive === ':hover' ? undefined : ':hover';
		_state.activeSelector = undefined;
		_state.showElementClasses = false;

		// Ensure pseudo-classes panel is visible (same as watcher behavior).
		if ( _state.pseudoClassActive ) {
			localStorage.setItem( 'brx_show_pseudo_classes', 'true' );
		}
	}

	/**
	 * Walk up the element tree from the active element to find its ancestor section.
	 * Returns the section element object, or null if not found.
	 */
	function findAncestorSection() {
		if ( ! _state || ! _state.activeElement ) {
			return null;
		}

		var current = _state.activeElement;

		while ( current ) {
			if ( current.name === 'section' ) {
				return current;
			}

			if ( ! current.parent ) {
				return null;
			}

			current = getElementById( current.parent );
		}

		return null;
	}

	/**
	 * Insert an element by name via the matching ELEMENT_SHORTCUTS entry.
	 */
	function insertElement( e ) {
		var elementName = ELEMENT_SHORTCUTS[ e.code ];

		if ( ! elementName ) {
			return;
		}

		e.preventDefault();

		var props = gp();
		var el = props.$_createElement( { name: elementName } );

		// When inserting a section, place it immediately after the current section
		// (or the section that contains the active element). Bricks' addNewElement
		// overwrites the index for sections, so we temporarily set the active element
		// to the ancestor section and enable insertAfter.
		if ( elementName === 'section' ) {
			var section = findAncestorSection();

			if ( section ) {
				_state.activeId = section.id;
				_state.insertAfter = true;

				props.$_addNewElement( { element: el } );

				_state.insertAfter = false;

				return;
			}
		}

		props.$_addNewElement( { element: el } );
	}

	/**
	 * Wrap selected element(s) with the configured wrap element (default: Block).
	 * Respects the same builderWrapElement setting as Bricks' CMD/CTRL+Shift+P.
	 */
	function wrapWithBlock( e ) {
		if ( ! _state.activeElement ) {
			return;
		}

		e.preventDefault();

		var wrapType = ( window.bricksData && window.bricksData.builderWrapElement ) || 'block';

		gp().$_wrapInNestable( null, wrapType );
	}

	/* -----------------------------------------------------------------------
	 * Unified keyboard dispatcher.
	 *
	 * A single keydown listener per document replaces the previous three.
	 * Guard logic (modifier keys, editable targets, command palette) is
	 * checked once, then the appropriate handler is called.
	 * ----------------------------------------------------------------------- */

	function handleKeydown( e ) {
		// Never intercept typing in form fields.
		if ( isEditableTarget( e ) ) {
			return;
		}

		// Alt+H — toggle :hover (allows Alt modifier, blocks others).
		if ( e.altKey && e.code === 'KeyH' && ! e.ctrlKey && ! e.metaKey && ! e.shiftKey && ! e.repeat ) {
			gp();
			toggleHover( e );
			return;
		}

		// Everything below requires NO modifier keys.
		if ( e.altKey || e.ctrlKey || e.metaKey || e.shiftKey || e.repeat ) {
			return;
		}

		if ( ! gp() || ! _state ) {
			return;
		}

		// Don't act when the command palette is open.
		if ( _state.showCommandPalette ) {
			return;
		}

		if ( e.code === 'KeyW' ) {
			wrapWithBlock( e );
			return;
		}

		if ( ELEMENT_SHORTCUTS[ e.code ] ) {
			insertElement( e );
		}
	}

	/**
	 * Attach the unified keydown listener to a document (main or iframe).
	 */
	function attachListeners( doc ) {
		doc.addEventListener( 'keydown', handleKeydown );
	}

	/* -----------------------------------------------------------------------
	 * Structure panel: require double-click to edit element labels.
	 *
	 * Bricks' Vue click handler sets a reactive `readonly` variable to false
	 * on every single click, and Vue then removes the DOM readonly attribute
	 * in its render cycle.
	 *
	 * Strategy: a MutationObserver always reverts readonly removal
	 * immediately. When we detect a double-click (via the dblclick event),
	 * we temporarily pause the observer, explicitly remove readonly, and
	 * resume the observer after a short delay.
	 * ----------------------------------------------------------------------- */

	function patchStructureLabelClick() {
		function applyPatch( panel ) {
			var paused = false;

			// MutationObserver: always revert readonly removal while not paused.
			var attrObserver = new MutationObserver( function ( mutations ) {
				if ( paused ) {
					return;
				}

				for ( var i = 0; i < mutations.length; i++ ) {
					var m = mutations[ i ];

					if (
						m.type === 'attributes' &&
						m.attributeName === 'readonly' &&
						m.target.tagName === 'INPUT' &&
						! m.target.hasAttribute( 'readonly' ) &&
						m.target.closest( '.structure-item .title' )
					) {
						m.target.setAttribute( 'readonly', '' );
						m.target.readOnly = true;
					}
				}
			} );

			attrObserver.observe( panel, {
				attributes: true,
				attributeFilter: [ 'readonly' ],
				subtree: true,
			} );

			// On double-click: pause the observer, explicitly enter edit mode.
			panel.addEventListener( 'dblclick', function ( e ) {
				var input = e.target.closest( '.structure-item .title input[type="text"]' );

				if ( ! input ) {
					return;
				}

				// Pause observer so our changes aren't reverted.
				paused = true;

				// Remove readonly to enter edit mode.
				input.removeAttribute( 'readonly' );
				input.readOnly = false;
				input.select();

				// Resume observer after Vue has settled.
				setTimeout( function () {
					paused = false;
				}, 200 );
			}, true );
		}

		// The structure panel may not exist yet. Watch for it.
		var existing = document.getElementById( 'bricks-structure' );

		if ( existing ) {
			applyPatch( existing );
			return;
		}

		var observer = new MutationObserver( function () {
			var panel = document.getElementById( 'bricks-structure' );

			if ( panel ) {
				applyPatch( panel );
				observer.disconnect();
			}
		} );

		observer.observe( document.body, { childList: true, subtree: true } );
	}

	/* -----------------------------------------------------------------------
	 * Auto-expand new Sections and select their Container.
	 *
	 * Wraps $_addNewElement so that when a Section is added (via any method —
	 * keyboard shortcut, + button, command palette) the new Section is
	 * expanded in the structure panel and its auto-created Container becomes
	 * the active element.
	 *
	 * Instead of relying on scrollToElementId (which only works when Bricks'
	 * structureAutoSync is enabled), we click the DOM toggle arrow directly,
	 * matching the approach used by Advanced Themer.
	 * ----------------------------------------------------------------------- */

	function patchAddNewElementForSections() {
		var props = gp();

		if ( ! props || ! props.$_addNewElement ) {
			return;
		}

		var original = props.$_addNewElement;

		// Idempotency guard — avoid double-wrapping if the script runs twice.
		if ( original.__bl_patched ) {
			return;
		}

		props.$_addNewElement = function () {
			var result = original.apply( this, arguments );

			// Only act when a section was just created.
			if ( ! result || result.name !== 'section' ) {
				return result;
			}

			var sectionId = result.id;

			// Wait one tick for Vue / Xp() to finish creating nestable children.
			setTimeout( function () {
				var section = getElementById( sectionId );

				if ( ! section || ! section.children || ! section.children.length ) {
					return;
				}

				// First child is the auto-created Container.
				var containerId = section.children[ 0 ];

				// Expand the section in the structure panel by clicking its toggle
				// arrow. This works regardless of the structureAutoSync setting.
				var toggle = document.querySelector(
					'#bricks-structure .element[data-id="' + sectionId + '"] .toggle'
				);

				if ( toggle && toggle.dataset.name === 'arrow-right' ) {
					toggle.click();
				}

				// Select the container.
				_state.activeId = containerId;
				_state.activePanel = 'element';
			}, 0 );

			return result;
		};

		props.$_addNewElement.__bl_patched = true;
	}

	/* -----------------------------------------------------------------------
	 * Initialisation.
	 * ----------------------------------------------------------------------- */

	// Listen on main document.
	attachListeners( document );

	// Also listen inside the builder canvas iframe (where focus usually is).
	var iframe = document.getElementById( 'bricks-builder-iframe' );

	if ( iframe ) {
		iframe.addEventListener( 'load', function () {
			try {
				attachListeners( iframe.contentDocument );
			} catch ( err ) {
				// Same-origin policy — should not happen on local dev.
			}
		} );

		// Iframe may already be loaded.
		try {
			if ( iframe.contentDocument && iframe.contentDocument.readyState === 'complete' ) {
				attachListeners( iframe.contentDocument );
			}
		} catch ( err ) {
			// Ignore.
		}
	}

	// Apply structure panel label patch.
	patchStructureLabelClick();

	// Patch addNewElement to auto-select container inside new sections.
	// Retries until Vue is fully mounted and the global property is available.
	function initPatches() {
		if ( ! gp() || ! gp().$_addNewElement ) {
			setTimeout( initPatches, 100 );
			return;
		}

		patchAddNewElementForSections();
	}

	initPatches();
} )();

保存后重新加载构建器(Reload the builder)即可生效。

别忘了 Bricks 自带的默认快捷键。配合上面的自定义快捷键,下面这四个默认快捷键尤其好用:

💡 译者注:这段 JavaScript 代码的功劳归 Claude(原文结尾注明 “Credit for the JavaScript code: Claude.”)。代码依赖 Bricks 构建器内部的 Vue 应用全局对象(__vue_app__$_state$_addNewElement 等),属于对 Bricks 内部 API 的调用,升级 Bricks 版本后如失效,需按新版内部结构调整。

需要帮忙?

用 Bricks 建站?我接客户项目。

从快速营销站到完整的 Bricks 建站,再到从 Elementor 迁移——我都做过。本站每篇教程都来自真实项目经验。告诉我你的需求,一个工作日内回复。

  • Bricks 建站与改版
  • Elementor / Divi → Bricks 迁移
  • Bricks → Astro / headless 性能升级
  • 速度优化,PageSpeed 95+ 目标