1.0 #4

Merged
m3i46ogeb merged 4 commits from develop into main 3 months ago

@ -5,54 +5,74 @@ import PopupModal from './components/PopupModal';
import SearchIndex from './search-index.js';
import i18nLib from '@tryghost/i18n';
/**
* 搜索应用的主组件 - 负责管理整个搜索功能的布局和状态
*/
export default class App extends React.Component {
constructor(props) {
super(props);
// ==================== 国际化设置 ====================
// 设置语言环境,默认为英语
const i18nLanguage = this.props.locale || 'en';
const i18n = i18nLib(i18nLanguage, 'search');
const dir = i18n.dir() || 'ltr';
const dir = i18n.dir() || 'ltr'; // 文字方向(左到右或右到左)
// ==================== 搜索索引初始化 ====================
// 创建搜索索引实例,用于处理搜索逻辑
const searchIndex = new SearchIndex({
adminUrl: props.adminUrl,
apiKey: props.apiKey,
dir: dir
adminUrl: props.adminUrl, // 管理后台URL
apiKey: props.apiKey, // API密钥
dir: dir // 文字方向
});
// ==================== 组件状态初始化 ====================
this.state = {
searchIndex,
showPopup: false,
indexStarted: false,
indexComplete: false,
t: i18n.t,
dir: dir,
scrollbarWidth: 0
searchIndex, // 搜索索引实例
showPopup: false, // 是否显示搜索弹窗
indexStarted: false, // 是否开始构建索引
indexComplete: false, // 索引构建是否完成
t: i18n.t, // 国际化翻译函数
dir: dir, // 文字方向
scrollbarWidth: 0 // 滚动条宽度(用于防止布局偏移)
};
// 创建输入框的引用用于直接操作DOM元素
this.inputRef = React.createRef();
}
/**
* 组件挂载后立即执行
*/
componentDidMount() {
// 计算并保存滚动条宽度
const scrollbarWidth = this.getScrollbarWidth();
this.setState({scrollbarWidth});
// 初始化搜索设置
this.initSetup();
}
/**
* 组件更新时执行主要用于处理弹窗显示/隐藏时的滚动条逻辑
*/
componentDidUpdate(_prevProps, prevState) {
// ==================== 弹窗显示状态变化处理 ====================
if (prevState.showPopup !== this.state.showPopup) {
/** Remove background scroll when popup is opened */
/** 当弹窗打开时移除背景滚动,防止页面抖动 */
try {
if (this.state.showPopup) {
/** When modal is opened, store current overflow and set as hidden */
/** 当弹窗打开时保存当前的overflow状态并设置为hidden */
this.bodyScroll = window.document?.body?.style?.overflow;
this.bodyMargin = window.getComputedStyle(document.body).getPropertyValue('margin-right');
window.document.body.style.overflow = 'hidden';
// 如果有滚动条且内容高度超过视口,调整右边距防止布局偏移
if (this.state.scrollbarWidth && document.body.scrollHeight > window.innerHeight) {
window.document.body.style.marginRight = `calc(${this.bodyMargin} + ${this.state.scrollbarWidth}px)`;
}
} else {
/** When the modal is hidden, reset overflow property for body */
/** 当弹窗隐藏时恢复body的overflow属性 */
window.document.body.style.overflow = this.bodyScroll || '';
if (!this.bodyMargin || this.bodyMargin === '0px') {
window.document.body.style.marginRight = '';
@ -61,74 +81,93 @@ export default class App extends React.Component {
}
}
} catch (e) {
/** Ignore any errors for scroll handling */
/** 忽略滚动处理中的任何错误 */
}
}
// ==================== 弹窗关闭时清空搜索值 ====================
if (this.state.showPopup !== prevState?.showPopup && !this.state.showPopup) {
this.setState({
searchValue: ''
});
}
// ==================== 弹窗打开时初始化搜索索引 ====================
if (this.state.showPopup && !this.state.indexStarted) {
this.setupSearchIndex();
}
}
/**
* 设置搜索索引 - 异步初始化搜索功能
*/
async setupSearchIndex() {
this.setState({
indexStarted: true
indexStarted: true // 标记索引构建开始
});
await this.state.searchIndex.init();
await this.state.searchIndex.init(); // 等待索引初始化完成
this.setState({
indexComplete: true
indexComplete: true // 标记索引构建完成
});
}
/**
* 组件卸载前的清理工作
*/
componentWillUnmount() {
/**Clear timeouts and event listeners on unmount */
/** 清除超时和事件监听器 */
window.removeEventListener('hashchange', this.hashHandler, false);
window.removeEventListener('keydown', this.handleKeyDown, false);
}
/**
* 初始化搜索设置
*/
initSetup() {
// Listen to preview mode changes
// 监听预览模式变化
this.handleSearchUrl();
this.addKeyboardShortcuts();
this.setupCustomTriggerButton();
// 哈希变化处理器用于URL路由
this.hashHandler = () => {
this.handleSearchUrl();
};
window.addEventListener('hashchange', this.hashHandler, false);
}
// User for adding trailing margin to prevent layout shift when popup appears
/**
* 计算滚动条宽度 - 用于防止弹窗出现时的布局偏移
*/
getScrollbarWidth() {
// Create a temporary div
// 创建临时div元素
const div = document.createElement('div');
div.style.visibility = 'hidden';
div.style.overflow = 'scroll'; // forcing scrollbar to appear
div.style.overflow = 'scroll'; // 强制显示滚动条
document.body.appendChild(div);
// Calculate the width difference
// 计算宽度差异(这就是滚动条的宽度)
const scrollbarWidth = div.offsetWidth - div.clientWidth;
// Clean up
// 清理临时元素
document.body.removeChild(div);
return scrollbarWidth;
}
/** Setup custom trigger buttons handling on page */
/**
* 设置页面上的自定义触发按钮处理
*/
setupCustomTriggerButton() {
// Handler for custom buttons
// 自定义按钮的点击处理器
this.clickHandler = (event) => {
event.preventDefault();
this.setState({
showPopup: true
showPopup: true // 显示搜索弹窗
});
// ==================== 焦点管理技巧 ====================
// 创建临时输入元素来解决焦点问题
const tmpElement = document.createElement('input');
tmpElement.style.opacity = '0';
tmpElement.style.position = 'fixed';
@ -136,12 +175,14 @@ export default class App extends React.Component {
document.body.appendChild(tmpElement);
tmpElement.focus();
// 延迟后将焦点转移到真正的搜索输入框
setTimeout(() => {
this.inputRef.current.focus();
document.body.removeChild(tmpElement);
}, 150);
};
// 获取所有自定义触发按钮并添加点击事件
this.customTriggerButtons = this.getCustomTriggerButtons();
this.customTriggerButtons.forEach((customTriggerButton) => {
customTriggerButton.removeEventListener('click', this.clickHandler);
@ -149,27 +190,39 @@ export default class App extends React.Component {
});
}
/**
* 获取页面上的自定义触发按钮
*/
getCustomTriggerButtons() {
const customTriggerSelector = '[data-ghost-search]';
const customTriggerSelector = '[data-ghost-search]'; // 通过data属性选择按钮
return document.querySelectorAll(customTriggerSelector) || [];
}
/**
* 处理搜索URL - 当URL哈希为/#search时自动打开搜索弹窗
*/
handleSearchUrl() {
const [path] = window.location.hash.substr(1).split('?');
if (path === '/search' || path === '/search/') {
this.setState({
showPopup: true
});
// 替换历史状态,移除哈希部分
window.history.replaceState('', document.title, window.location.pathname);
}
}
/**
* 添加快捷键支持 - Cmd+K 打开搜索
*/
addKeyboardShortcuts() {
const customTriggerButtons = this.getCustomTriggerButtons();
if (!customTriggerButtons?.length) {
return;
}
this.handleKeyDown = (e) => {
// 检测 Cmd+K (Mac) 或 Ctrl+K (Windows)
if (e.key === 'k' && e.metaKey) {
this.setState({
showPopup: true
@ -182,19 +235,26 @@ export default class App extends React.Component {
document.addEventListener('keydown', this.handleKeyDown);
}
/**
* 渲染组件 - 这里是整个搜索应用的布局入口
*/
render() {
return (
/**
* 使用Context API提供全局状态给所有子组件
* 这是React中管理全局状态的常用模式
*/
<AppContext.Provider value={{
page: 'search',
showPopup: this.state.showPopup,
adminUrl: this.props.adminUrl,
stylesUrl: this.props.stylesUrl,
searchIndex: this.state.searchIndex,
indexComplete: this.state.indexComplete,
searchValue: this.state.searchValue,
inputRef: this.inputRef,
onAction: () => {},
dispatch: (action, data) => {
page: 'search', // 当前页面标识
showPopup: this.state.showPopup, // 弹窗显示状态
adminUrl: this.props.adminUrl, // 管理后台URL
stylesUrl: this.props.stylesUrl, // 样式文件URL
searchIndex: this.state.searchIndex, // 搜索索引实例
indexComplete: this.state.indexComplete, // 索引完成状态
searchValue: this.state.searchValue, // 搜索关键词
inputRef: this.inputRef, // 输入框引用
onAction: () => {}, // 动作处理器
dispatch: (action, data) => { // 状态更新函数
if (action === 'update') {
this.setState({
...this.state,
@ -202,11 +262,12 @@ export default class App extends React.Component {
});
}
},
t: this.state.t,
dir: this.state.dir
t: this.state.t, // 翻译函数
dir: this.state.dir // 文字方向
}}>
{/* 渲染搜索弹窗组件 - 这是主要的UI组件 */}
<PopupModal />
</AppContext.Provider>
);
}
}
}
Loading…
Cancel
Save