“写笔记”支持四种格式——Word 文档、Excel 表格、Markdown、纯文本,起稿或二次编辑时都能随时切换,同一篇笔记想用哪种形态来记,都由你说了算。
md、txt、csv、json 这类纯文本则原样载入,不做多余加工。拿一张现成的表倒进来、改几笔、再导出去,等于白用一台免费的格式转换器。
要带走就在右上角点“下载”,可导出 PDF、Word、Markdown、Excel、TXT 等格式;列表卡片“⋯”菜单里,也有同样的下载入口。
在“工具”页点“+ 上传工具”即可发布:填好名称与链接,再用 Markdown 把使用方法写清楚——能解决什么问题、怎么装、怎么用,比堆介绍实在。
要分发安装包就一并上传压缩包(ZIP、RAR、7Z、TAR.GZ,最大 35MB),别人在详情页一键下载;只放链接不带附件也可以。
工具按大家的收藏热度排序,好用的自然会被顶上来。发布后可在详情页或卡片菜单里编辑、下架。
写笔记时勾上“隐藏”,这篇就只存在于你自己的账号里:不进列表、不进搜索、不上首页精选,也不会出现在任何公开的页面,链接发给别人同样打不开。
适合放密码、草稿、日记这类只给自己看的内容;想公开,去“发布”打开它,把“隐藏”的勾去掉再保存,之后编辑会默认保持原状态,不会悄悄变回公开。
你的内容会同时保存在多个副本上,系统定期做备份与完整性校验,再配合异地容灾机制:就算某台机器出问题,数据也不会丢,可以长期放心存放;特别重要的资料,仍建议你另外再留一份备份。
全站跑在容器化、模块化的现代架构上,更新、部署、回滚都很快,扩展性和稳定性都按长期运营的标准来设计(Built for reliability, designed to scale)。
这个网站最早只是一个人的笔记仓库,后来慢慢长成现在的知识中枢。设计上很克制——没有广告、没有追踪、没有推荐算法,只是干干净净地存放一些东西;既然做好了,就公开出来,万一有人用得上呢。
不做大而全,不做平台梦,保持简单、保持克制、保持好奇。所有内容都由用户贡献、由用户维护:不会突然冒出付费墙,不会在角落塞广告位,也不会把你的数据卖给第三方。
产品会持续迭代,站内日志页记录着每一次改动,改了什么都有迹可循;想了解这个站是怎么一步步走到今天的,翻翻日志就能看到来龙去脉。
如果在这里看到涉嫌违规的内容,点对应卡片右侧的“举报”按钮就能提交,我们会尽快核实处理;也谢谢你花一点时间,一起把这里维护干净。
如何用AJAX构建Shopify购物车抽屉(2026版)
最近在给一个Shopify店铺做优化,发现大多数教程和AI工具生成的购物车抽屉都太脆弱了——它们用JavaScript变量维护一个计数器,然后手动更新DOM。这在demo里看着挺好,但一旦遇到订单合并、折扣生效或者变体售罄,立马就崩了。今天我就分享一个更靠谱的做法,服务器永远是唯一的数据源,外加2026年的新玩法,让你的抽屉能和App以及AI购物代理“说同一种语言”。
先说说为什么要做这个。默认情况下,在Shopify店铺里点击“加入购物车”,整个页面会重新加载。想象一下,顾客正盯着你的产品,点一下“加入购物车”,然后浏览器把当前页面扔掉,重新构建——在慢速网络下,这就意味着两三秒的空白屏幕。有时候甚至直接跳转到 /cart 页面,顾客完全离开了他们正在浏览的商品,购买冲动瞬间就没了。
购物车抽屉就是解决这个问题的。它是一个滑出的面板,顾客添加商品后自动出现:购物车实时更新,“结算”按钮就在眼前,顾客永远不会离开当前页面。几乎所有高转化率的Shopify店铺都有这个功能,而且你完全不需要安装App就能实现——只需要两个Ajax端点,加上不到一百行JavaScript代码。
如果你想要跟着做,先打开一个可以编辑的开发主题,我们一步步搭建。所有代码都基于标准的Online Store 2.0主题(Horizon或Dawn),不安装任何App。
目录
- 你要构建什么
- 第一步:抽屉的标记结构
- 第二步:无刷新添加购物车
- 第三步:从服务器数据源渲染抽屉
- 第四步:数量加减与事件委托
- 第五步:删除商品行与清空购物车
- 第六步:用Section Rendering API重新渲染(可上生产环境的版本)
- 2026升级:标准店铺前端事件与操作
- 为什么这很重要
- 完整文件
- 总结
你要构建什么
最终,你会得到一个具备以下功能的抽屉:
- 通过Ajax添加购物车,无需页面刷新
- 每次变更后重新读取购物车,把服务器返回的数据当作唯一真实来源
- 自行渲染内容并滑出
- 用单个委托监听器处理数量加减
- 支持删除商品行和清空购物车
- 最终升级版:通过Shopify新的标准店铺前端操作暴露自身,让App和AI代理能够驱动它
前提条件
- 一个可编辑的Shopify主题(示例基于Online Store 2.0,如Horizon或Dawn)
- 熟悉
fetch和Promise - 基本的Liquid知识
- 不需要App、不需要框架、不需要构建步骤
第一步:抽屉的标记结构
我们要把抽屉做成一个section,这样它会在每个页面都渲染,并且在JavaScript运行之前就用Liquid在服务器端渲染出当前购物车的内容。这一点很重要:如果顾客到达时购物车里已经有商品,抽屉在首次渲染时就是正确的,JavaScript只需要在变更后更新它。这就是渐进增强,也是大多数自己动手做的方案所忽略的。
下面这些 data-* 属性是标记和脚本之间的约定。JavaScript要操作的所有元素,都通过这些属性来查找,从不依赖类名或标签位置。
{%- comment -%}
sections/cart-drawer.liquid
{%- endcomment -%}
<button type="button" class="cart-toggle" data-cart-toggle>
Cart
<span class="cart-count" data-cart-count>{{ cart.item_count }}</span>
</button>
<aside class="drawer" data-drawer aria-label="Cart" aria-hidden="true">
<div class="drawer__head">
<h2>Your cart</h2>
<button type="button" class="drawer__close" data-drawer-close aria-label="Close cart">×</button>
</div>
<div class="drawer__body">
<p class="drawer__empty" data-drawer-empty {% if cart.item_count > 0 %}style="display:none"{% endif %}>
Your cart is empty.
</p>
<ul class="drawer__items" data-drawer-items>
{%- for item in cart.items -%}
<li class="drawer__item" data-line data-line-key="{{ item.key }}" data-quantity="{{ item.quantity }}">
{{ item.image | image_url: width: 96 | image_tag: class: 'drawer__item-img', loading: 'lazy', alt: item.product.title }}
<span class="drawer__item-title">{{ item.product.title }}</span>
<span class="drawer__item-price">{{ item.final_line_price | money }}</span>
</li>
{%- endfor -%}
</ul>
</div>
<div class="drawer__foot">
<div class="drawer__subtotal">
<span>Subtotal</span>
<span data-cart-subtotal>{{ cart.total_price | money }}</span>
</div>
<p class="drawer__ship">Shipping & taxes calculated at checkout.</p>
<a href="{{ routes.cart_url }}" class="drawer__checkout">Checkout</a>
</div>
</aside>
<div class="drawer__scrim" data-drawer-scrim></div>
这里有两个细节值得注意:商品行使用的是 item.final_line_price,而不是 item.line_price。这两个属性在购物车对象中都存在,但 final_line_price 反映了行级别的折扣,是顾客实际需要支付的金额。另外,每个 <li> 都带有 data-line-key 和 data-quantity 属性,稍后数量和删除控件会读取这些属性。
“加入购物车”按钮放在商品卡片或商品详情页上,它直接从Liquid获取变体ID,确保每次添加的都是正确的变体:
{%- comment -%}
在商品卡片或商品详情页中
{%- endcomment -%}
<button type="button" class="pdp__add" data-add data-variant-id="{{ product.selected_or_first_available_variant.id }}">
Add to cart
</button>
第二步:无刷新添加购物车
整个模式可以用一句话概括:变更购物车,然后重新读取,再渲染并打开。具体来说,就是先 POST /cart/add.js 添加变体,再 GET /cart.js 读取完整的购物车数据,最后用返回的数据渲染抽屉。
// assets/cart-drawer.js
document.querySelectorAll("[data-add]").forEach(function (btn) {
btn.addEventListener("click", function () {
var id = Number(btn.getAttribute("data-variant-id"));
fetch("/cart/add.js", {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({
id: id,
quantity: 1
}),
})
.then(function (r) {
return r.json();
})
.then(function () {
return refresh(); // 重新读取 /cart.js
})
.then(openDrawer); // 然后滑出抽屉
});
});
你可能会问:既然 /cart/add.js 已经返回了数据,为什么还要重新读取购物车?因为 add.js 返回的只是你刚刚添加的那一行数据,不是完整的购物车内容,而抽屉显示的是整个购物车。更重要的是,Shopify才是最终决定购物车状态的一方。它可能把你的新商品合并到已有的商品行中,应用自动折扣,或者拒绝一个已售罄的变体。要确保抽屉和实际情况一致,唯一的方法就是向服务器请求真实数据。
这就是 refresh() 函数做的事情,它也是整个文件中唯一允许操作购物车UI的函数:
function refresh() {
return fetch("/cart.js")
.then(function (r) {
return r.json();
})
.then(render);
}
这里用到的两个端点(/cart/add.js 和 /cart.js)都是Shopify Ajax API的一部分,每个店铺前端都可以直接使用,无需任何配置。
变更→重新读取→渲染。添加商品、修改数量、删除商品都遵循同样的循环。
第三步:从服务器数据源渲染抽屉
render(cart) 函数接收 /cart.js 返回的数据,然后用它来渲染抽屉。注意它不做的事情:它从不自己计算总价或递增计数器。它直接从Shopify返回的对象中读取 item_count 和 total_price。
function money(cents) {
return "$" + (cents / 100).toFixed(2);
}
function render(cart) {
// 更新购物车计数
document.querySelector("[data-cart-count]").textContent = cart.item_count;
// 更新小计金额
document.querySelector("[data-cart-subtotal]").textContent = money(cart.total_price);
var itemsEl = document.querySelector("[data-drawer-items]");
var emptyEl = document.querySelector("[data-drawer-empty]");
// 清空现有商品列表
itemsEl.innerHTML = "";
// 如果购物车为空
if (!cart.items.length) {
emptyEl.style.display = "block";
return;
}
emptyEl.style.display = "none";
// 遍历每个商品行并创建DOM元素
cart.items.forEach(function (line) {
var li = document.createElement("li");
li.className = "drawer__item";
li.setAttribute("data-line", "");
li.setAttribute("data-line-key", line.key);
li.setAttribute("data-quantity", line.quantity);
// 商品图片
var img = document.createElement("img");
img.src = line.image;
img.width = 96;
img.alt = line.product_title;
img.className = "drawer__item-img";
li.appendChild(img);
// 商品标题
var title = document.createElement("span");
title.className = "drawer__item-title";
title.textContent = line.product_title;
li.appendChild(title);
// 商品价格
var price = document.createElement("span");
price.className = "drawer__item-price";
price.textContent = money(line.line_price);
li.appendChild(price);
itemsEl.appendChild(li);
});
}
这里有个关键点:money() 函数只是简单的美元格式化。如果你的店铺使用其他货币,需要相应调整。另外注意我用了 line.line_price 而不是 line.final_line_price 来显示——这里可以根据需求选择,但如果你在行级别有折扣,应该用 final_line_price。
第四步:数量加减与事件委托
传统的做法是为每个加减按钮单独绑定事件监听器,但这样在新增商品行后需要重新绑定。更好的做法是使用事件委托:在父容器上监听点击事件,然后根据点击的目标元素来判断是增加还是减少数量。
document.querySelector("[data-drawer-items]").addEventListener("click", function (e) {
var target = e.target;
var lineItem = target.closest("[data-line]");
if (!lineItem) return;
var key = lineItem.getAttribute("data-line-key");
var currentQty = parseInt(lineItem.getAttribute("data-quantity"), 10);
var newQty;
if (target.hasAttribute("data-qty-minus")) {
newQty = Math.max(0, currentQty - 1);
} else if (target.hasAttribute("data-qty-plus")) {
newQty = currentQty + 1;
} else {
return;
}
// 如果数量变为0,删除该商品行
if (newQty === 0) {
removeLineItem(key);
} else {
updateLineItem(key, newQty);
}
});
这里用到了 closest() 方法,它会向上查找最近的匹配选择器的祖先元素。这样即使加减按钮嵌套在多层结构中也能正常工作。
对应的 updateLineItem 和 removeLineItem 函数如下:
function updateLineItem(key, quantity) {
fetch("/cart/change.js", {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({
id: key,
quantity: quantity
}),
})
.then(function (r) {
return r.json();
})
.then(render)
.then(openDrawer);
}
function removeLineItem(key) {
fetch("/cart/change.js", {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({
id: key,
quantity: 0
}),
})
.then(function (r) {
return r.json();
})
.then(render)
.then(openDrawer);
}
注意这里我用了 /cart/change.js 端点,它接受一个 id(商品行的key)和 quantity。当 quantity 设为0时,相当于删除该商品行。这个端点比 /cart/update.js 更精确,因为它只修改指定的商品行,不会影响其他行。
第五步:删除商品行与清空购物车
除了通过数量减到0来删除商品行外,我们还应该提供一个显式的删除按钮。在商品行中添加一个删除按钮:
<li class="drawer__item" data-line data-line-key="{{ item.key }}" data-quantity="{{ item.quantity }}">
<!-- 其他内容 -->
<button type="button" data-remove aria-label="Remove item">×</button>
</li>
然后在事件委托中处理:
document.querySelector("[data-drawer-items]").addEventListener("click", function (e) {
var target = e.target;
var lineItem = target.closest("[data-line]");
if (!lineItem) return;
var key = lineItem.getAttribute("data-line-key");
if (target.hasAttribute("data-remove")) {
removeLineItem(key);
return;
}
// 数量加减逻辑...
});
清空购物车则更简单,直接调用 /cart/clear.js:
function clearCart() {
fetch("/cart/clear.js", {
method: "POST"
})
.then(function (r) {
return r.json();
})
.then(render)
.then(closeDrawer);
}
注意清空购物车后我调用了 closeDrawer(),因为此时抽屉里没有商品了,显示空购物车没有意义。当然,你也可以选择不关闭,让顾客看到“您的购物车是空的”提示。
第六步:用Section Rendering API重新渲染(可上生产环境的版本)
到目前为止,我们一直在用JavaScript手动构建DOM元素。这在小规模场景下没问题,但如果你的商品行包含复杂的内容(比如自定义字段、捆绑销售信息、个性化选项),手动构建DOM会变得非常繁琐且容易出错。
Shopify的Section Rendering API提供了一个更好的方案:让服务器渲染HTML片段,然后直接插入到DOM中。这样你就不需要在JavaScript中处理HTML字符串拼接了。
首先,在 sections/cart-drawer.liquid 中添加一个用于渲染单个商品行的代码块:
{%- comment -%}
这个部分会被Section Rendering API调用
{%- endcomment -%}
{%- if request.design_mode == false -%}
{%- if request.page_type == 'cart-drawer-item' -%}
{%- assign item = cart.items | first -%}
<li class="drawer__item" data-line data-line-key="{{ item.key }}" data-quantity="{{ item.quantity }}">
{{ item.image | image_url: width: 96 | image_tag: class: 'drawer__item-img', loading: 'lazy', alt: item.product.title }}
<span class="drawer__item-title">{{ item.product.title }}</span>
<span class="drawer__item-price">{{ item.final_line_price | money }}</span>
<button type="button" data-remove aria-label="Remove item">×</button>
</li>
{%- endif -%}
{%- endif -%}
然后修改JavaScript中的 render 函数,使用Section Rendering API来获取渲染好的HTML:
function render(cart) {
// 更新计数和小计
document.querySelector("[data-cart-count]").textContent = cart.item_count;
document.querySelector("[data-cart-subtotal]").textContent = money(cart.total_price);
var itemsEl = document.querySelector("[data-drawer-items]");
var emptyEl = document.querySelector("[data-drawer-empty]");
if (!cart.items.length) {
itemsEl.innerHTML = "";
emptyEl.style.display = "block";
return;
}
emptyEl.style.display = "none";
// 使用Section Rendering API获取渲染好的HTML
var url = "/?sections=cart-drawer";
fetch(url)
.then(function (r) {
return r.json();
})
.then(function (sections) {
var html = sections["cart-drawer"];
var parser = new DOMParser();
var doc = parser.parseFromString(html, "text/html");
var newItems = doc.querySelector("[data-drawer-items]");
if (newItems) {
itemsEl.innerHTML = newItems.innerHTML;
}
});
}
这个方法的优势在于:你可以在Liquid模板中使用所有Shopify的过滤器、自定义字段和逻辑,JavaScript只需要负责插入最终的HTML。而且Section Rendering API返回的是JSON对象,其中包含了每个section的渲染结果,你可以只提取需要的部分。
不过要注意,Section Rendering API会触发完整的页面渲染,对于高频操作(比如数量加减)来说可能有点慢。在实际项目中,你可以混合使用两种方法:初始渲染和添加商品时用Section Rendering API,数量加减时用JavaScript手动更新。
2026升级:标准店铺前端事件与操作
现在到了最酷的部分。2026年,Shopify引入了标准店铺前端事件(Standard Storefront Events),这是一个统一的事件系统,让不同组件之间可以互相通信,而不需要直接耦合。
想象一下这样的场景:你安装了一个AI购物助手App,它想在你的购物车抽屉中添加商品。如果没有标准事件,这个App需要知道你的JavaScript函数叫什么名字,或者需要直接操作DOM——这既脆弱又不安全。
有了标准事件,一切变得简单:
// 监听标准 add-to-cart 事件
document.addEventListener("shopify:add-to-cart", function (e) {
var detail = e.detail;
// detail.variantId 是变体ID
// detail.quantity 是数量
// detail.properties 是自定义属性
fetch("/cart/add.js", {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({
id: detail.variantId,
quantity: detail.quantity || 1,
properties: detail.properties || {}
}),
})
.then(function (r) {
return r.json();
})
.then(function () {
return refresh();
})
.then(openDrawer);
});
// 监听标准 cart-open 事件
document.addEventListener("shopify:cart-open", function () {
refresh().then(openDrawer);
});
// 监听标准 cart-close 事件
document.addEventListener("shopify:cart-close", function () {
closeDrawer();
});
同样,你的抽屉也可以触发标准事件,让其他组件知道发生了什么:
function openDrawer() {
document.querySelector("[data-drawer]").setAttribute("aria-hidden", "false");
document.querySelector("[data-drawer-scrim]").style.display = "block";
document.body.style.overflow = "hidden";
// 触发标准事件
document.dispatchEvent(new CustomEvent("shopify:cart-opened", {
detail: {
timestamp: Date.now()
}
}));
}
function closeDrawer() {
document.querySelector("[data-drawer]").setAttribute("aria-hidden", "true");
document.querySelector("[data-drawer-scrim]").style.display = "none";
document.body.style.overflow = "";
// 触发标准事件
document.dispatchEvent(new CustomEvent("shopify:cart-closed", {
detail: {
timestamp: Date.now()
}
}));
}
这里的关键点是 e.detail 属性。CustomEvent 的 detail 属性可以携带任意数据,这样事件的发送方和接收方之间就有了一个清晰的契约。比如,AI购物代理可以这样触发添加购物车:
// AI购物代理的代码
document.dispatchEvent(new CustomEvent("shopify:add-to-cart", {
detail: {
variantId: 123456789,
quantity: 2,
properties: {
"Gift wrap": "Yes"
}
}
}));
你的抽屉代码不需要知道这个事件是从哪里来的——按钮点击、AI代理、还是其他App——它只需要监听并响应即可。
为什么这很重要
你可能会想,我为什么要费这么大劲用标准事件?直接用函数调用不是更简单吗?
短期来看确实如此。但考虑到Shopify生态系统的未来,标准事件有几个关键优势:
解耦:你的抽屉不需要知道谁在调用它。按钮、App、AI代理都可以通过同一套事件接口与你的抽屉交互。
可组合性:你可以把不同的功能模块组合在一起,而不需要修改彼此的代码。比如,你可以用一个“一键补货”App、一个“购物车推荐”App和一个AI购物助手,它们都通过标准事件与你的抽屉通信。
未来兼容:Shopify正在推动标准事件成为行业规范。如果你的代码遵循这个规范,未来新的App和工具就能直接兼容,而不需要你修改代码。
调试友好:标准事件可以在浏览器开发者工具中监听和调试。你可以打开控制台,输入
monitorEvents(document, 'shopify:')来查看所有触发的事件。
完整文件
以下是完整的 cart-drawer.js 文件,包含了所有功能:
// assets/cart-drawer.js
(function () {
'use strict';
// ===== 工具函数 =====
function money(cents) {
return "$" + (cents / 100).toFixed(2);
}
// ===== 核心函数 =====
function refresh() {
return fetch("/cart.js")
.then(function (r) {
return r.json();
})
.then(render);
}
function render(cart) {
document.querySelector("[data-cart-count]").textContent = cart.item_count;
document.querySelector("[data-cart-subtotal]").textContent = money(cart.total_price);
var itemsEl = document.querySelector("[data-drawer-items]");
var emptyEl = document.querySelector("[data-drawer-empty]");
itemsEl.innerHTML = "";
if (!cart.items.length) {
emptyEl.style.display = "block";
return;
}
emptyEl.style.display = "none";
cart.items.forEach(function (line) {
var li = document.createElement("li");
li.className = "drawer__item";
li.setAttribute("data-line", "");
li.setAttribute("data-line-key", line.key);
li.setAttribute("data-quantity", line.quantity);
var img = document.createElement("img");
img.src = line.image;
img.width = 96;
img.alt = line.product_title;
img.className = "drawer__item-img";
li.appendChild(img);
var title = document.createElement("span");
title.className = "drawer__item-title";
title.textContent = line.product_title;
li.appendChild(title);
var price = document.createElement("span");
price.className = "drawer__item-price";
price.textContent = money(line.line_price);
li.appendChild(price);
var removeBtn = document.createElement("button");
removeBtn.type = "button";
removeBtn.setAttribute("data-remove", "");
removeBtn.setAttribute("aria-label", "Remove item");
removeBtn.innerHTML = "×";
li.appendChild(removeBtn);
itemsEl.appendChild(li);
});
}
function updateLineItem(key, quantity) {
fetch("/cart/change.js", {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({
id: key,
quantity: quantity
}),
})
.then(function (r) {
return r.json();
})
.then(render)
.then(openDrawer);
}
function removeLineItem(key) {
fetch("/cart/change.js", {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({
id: key,
quantity: 0
}),
})
.then(function (r) {
return r.json();
})
.then(render)
.then(openDrawer);
}
function clearCart() {
fetch("/cart/clear.js", {
method: "POST"
})
.then(function (r) {
return r.json();
})
.then(render)
.then(closeDrawer);
}
// ===== UI 控制 =====
function openDrawer() {
document.querySelector("[data-drawer]").setAttribute("aria-hidden", "false");
document.querySelector("[data-drawer-scrim]").style.display = "block";
document.body.style.overflow = "hidden";
document.dispatchEvent(new CustomEvent("shopify:cart-opened", {
detail: { timestamp: Date.now() }
}));
}
function closeDrawer() {
document.querySelector("[data-drawer]").setAttribute("aria-hidden", "true");
document.querySelector("[data-drawer-scrim]").style.display = "none";
document.body.style.overflow = "";
document.dispatchEvent(new CustomEvent("shopify:cart-closed", {
detail: { timestamp: Date.now() }
}));
}
// ===== 事件绑定 =====
// 添加购物车按钮
document.querySelectorAll("[data-add]").forEach(function (btn) {
btn.addEventListener("click", function () {
var id = Number(btn.getAttribute("data-variant-id"));
fetch("/cart/add.js", {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({
id: id,
quantity: 1
}),
})
.then(function (r) {
return r.json();
})
.then(function () {
return refresh();
})
.then(openDrawer);
});
});
// 打开/关闭抽屉
document.querySelector("[data-cart-toggle]").addEventListener("click", function () {
var drawer = document.querySelector("[data-drawer]");
if (drawer.getAttribute("aria-hidden") === "true") {