如何在Intro.js引导步骤中渲染HTML而非纯文本(Symfony+EasyAdmin项目场景)
如何在Intro.js引导步骤中渲染HTML而非纯文本(Symfony+EasyAdmin项目场景)
嘿,我之前也碰到过几乎一模一样的问题!这个问题其实分两部分:一是EasyAdmin详情页的HTML渲染,二是Intro.js本身的HTML解析配置,咱们一个个解决:
1. 解决EasyAdmin详情页iframe显示为纯文本的问题
EasyAdmin默认会对所有字段内容进行HTML转义,防止XSS风险,但这就导致你的iframe代码被当成纯文本展示了。咱们只需要在CRUD控制器的字段配置中,给intro字段关闭HTML转义就行:
修改你的Page(或Step)的CRUD控制器
如果是在Page实体的CRUD控制器中配置详情页的steps展示:
// src/Controller/Admin/PageCrudController.php public function configureFields(string $pageName): iterable { // ... 其他字段配置 if ($pageName === Crud::PAGE_DETAIL) { yield CollectionField::new('steps', 'Steps') ->setEntryIsComplex(true) ->setEntryOptions([ 'fields' => [ Field::new('title'), Field::new('element'), // 关键:关闭intro字段的HTML转义 Field::new('intro')->setEscapeHtml(false), ], ]); } else { // 你的表单页原有配置保持不变 yield CollectionField::new('steps', 'Steps') ->setEntryType(IntroStepType::class) ->allowAdd() ->allowDelete() ->renderExpanded() ->addJsFiles(Asset::fromEasyAdminAssetPackage('field-text-editor.js')->onlyOnForms()) ->addCssFiles(Asset::fromEasyAdminAssetPackage('field-text-editor.css')->onlyOnForms()); } }
如果是在Step实体的独立CRUD控制器中:
// src/Controller/Admin/StepCrudController.php public function configureFields(string $pageName): iterable { yield Field::new('title'); yield Field::new('element'); // 直接给intro字段关闭转义 yield Field::new('intro')->setEscapeHtml(false); }
这样修改后,EasyAdmin的详情页就能正确渲染iframe了。
2. 解决Intro.js引导步骤中HTML不渲染的问题
Intro.js默认会把步骤的intro内容当成纯文本处理,咱们只需要在Stimulus控制器里给Intro.js添加一个配置,告诉它允许解析HTML内容:
修改你的Stimulus控制器
你可以选择全局关闭所有步骤的HTML转义,或者给单个步骤配置,推荐全局配置更简单:
// 你的stimulus控制器文件 import { Controller } from '@hotwired/stimulus'; import introJs from 'intro.js'; import '../vendor/intro.js/introjs.css'; export default class extends Controller { async connect() { this.intro = introJs(); // 给基础配置加上escape: false,全局允许HTML渲染 this.baseOptions = { showBullets: false, showPrevButton: true, nextLabel: 'Next', prevLabel: 'Previous', escape: false, // 关键:关闭HTML转义,让Intro.js渲染HTML内容 }; this.intro.setOptions(this.baseOptions); const params = new URLSearchParams(window.location.search); if ('true' === params.get('tutorial')) { await this.startTutorial(); } } // 剩下的方法保持不变,startTutorial里的options会继承baseOptions的escape配置 async startTutorial() { const path = encodeURIComponent(window.location.pathname); try { const response = await fetch(`/tutorial?path=${path}`); if (!response.ok) { console.error('Error loading tutorial data:', response.status, response.statusText); return; } const data = await response.json(); if (!data.steps?.length) return; const steps = data.steps.filter(step => !step.element || document.querySelector(step.element)); if (steps.length === 0) return; const options = { ...this.baseOptions, steps, doneLabel: data.next ? 'Next' : 'Done', }; this.intro.setOptions(options); // ... 剩下的代码不变 } catch (error) { console.error('Network error:', error); } } // 其他方法不变 removeTutorialQueryParam() { const url = new URL(window.location); url.searchParams.delete('tutorial'); window.history.replaceState({}, document.title, url); } }
如果只需要给特定步骤开启HTML渲染,也可以在startTutorial里对steps数组做映射:
const steps = data.steps.filter(step => !step.element || document.querySelector(step.element)) .map(step => ({ ...step, escape: false // 给单个步骤设置允许HTML渲染 }));
重要安全提醒
关闭HTML转义后,要确保只有可信的用户(比如管理员)能编辑intro字段,避免恶意用户插入危险的HTML/JS代码导致XSS攻击。如果需要对外公开,建议对输入的内容做过滤,比如只允许来自YouTube的iframe标签。
内容来源于stack exchange
相关产品推荐
相关产品推荐

