feat(细节三): 确认框动画与背景位移、主菜单防误触高亮、头像阻塞加载、弹窗滚动链
① 三个(实为四个)sweetalert2 确认框(设置权限/重命名/删除/删相册):保留原实现,只改动画 + 斩断它对 body 的动手脚。四处 Swal.fire 加 heightAuto:false + scrollbarPadding:false; common.less 里把 .swal2-show/.swal2-hide 换成与 x-modal 同一套(桌面 300ms ease-out 淡入+ scale .95→1、200ms ease-in 淡出;<768px 换成 translateY(16px)),遮罩过渡同源,并用 html body.swal2-shown:not(...) 兑掉 sweetalert2 对 body 的 overflow/padding 注入(不用 !important)。 实测:修前 4 个弹窗×2 视口都会让整页位移(桌面滚动条消失 +8px、手机注入 padding-right 8px 且 文档高 +20px、工具栏换行下移);修后逐项相等(关闭后复原也逐项相等)。动画实测 opacity 落在与 x-modal 同一条贝塞尔曲线上(如 ct=100/300 实测 0.6853 vs 曲线 0.6851,线性应为 0.3333)。 ② 长按主菜单防误触高亮:把二级菜单那套手势级 arming 复用过来(menu-arming,420ms + 抬手后 260ms, 跟手势走),只在「这一发菜单确实紧随一次触摸」时武装,桌面右键路径一行都不走。 CDP 真触摸实测(390x844、iPhone UA、手指压在菜单项上):修前 elementFromPoint 命中 <a>(会高亮, 钉住粘滞 :hover 后整行变蓝 = 老师截图那个现象);修后按住期间/抬手+60ms 命中的都是外层容器、 不跳色,抬手+480ms 恢复正常可点;冗余窗内真点「复制图片」仍触发、点「复制链接」仍就地展开子菜单、 桌面鼠标 hover/点击行为与改前逐项一致。 ③ 右上角头像:真因查明 —— 它是外部服务 https://cravatar.cn/avatar/<md5(email)>?s=96&d=mp&r=g (Utils::getAvatar),实测 376B、0.85~0.96s、每次 1 跳重定向,且页面 load 事件被它拖住(实测 慢源下 load 3214ms)、加载失败时没有任何兜底(所以空着)。改法:先渲染固定 28×28 占位(不抖动), 真实地址放 data-avatar-src,页面 load 之后再异步接上;加 decoding=async + onerror 失败兜底 (名字首字色块圆圈)、成功则移除首字占位。实测:慢源下 load 3214ms → 470ms(不再等它), 404 时从"空白"变成"首字圆圈",且头像块与整个按钮的位置尺寸与修前逐项相等。 ④ 手机在弹窗内下滑底部空一块:真因是**滚动链**——弹窗内滚到尽头后滚动传给背后文档(实测修前 document.scrollTop 从 0 冲到 2628),安卓上地址栏随之收起、底部露出空带。修法:弹窗滚动容器加 overscroll-contain 阻断接力;高度链上残留的 100vh 换成「同一块内先 vh 后 dvh」的内联声明 (CSSOM 实测 min-height 解析为 100dvh,祖先链已无 vh)。实测:修后 6 组手势(手机/桌面 × 长/短 内容 × wheel/touch)背后文档 scrollTop 恒为 0,弹窗内仍能正常滚到 maxScroll;桌面各项与改前逐项一致。 验证:longpress 79/79、sidebar 6/6、images-modal 35/35、images-modal-dom 35/35、swal-theme 48/48; webpack compiled successfully;补丁钉住值同步为 context-js.js 3614ec17…、images.blade.php 09ba35f3…, 补丁重生成 2802 行且「打回上游 = 仓库文件」字节一致。另清理了子代理遗留的调试脚本 test/dbg-gap.mjs。
This commit is contained in:
1 parent
5773d432fb
commit
db05433596
10 files changed
+621
-47
No files matched your search
+5
-5
@@ -107,9 +107,9 @@ RUN php -r "file_exists('.env') || copy('.env.example', '.env');" \
|
||||
# 改 src/ 里这三个文件后必须同步更新这里的 md5(故意做成"改了不更新就构建失败"),
|
||||
# 并重新生成 patches/ios-longpress.patch(tools/diff-vs-upstream.sh)。
|
||||
RUN printf '%s\n' \
|
||||
'b916555ab122d2b3db99153d9316a887 ./public/js/context-js/context-js.js' \
|
||||
'b916555ab122d2b3db99153d9316a887 ./resources/js/context-js.js' \
|
||||
'00f38dbf29c3d5cd9800dfd1d6709fb9 ./resources/views/user/images.blade.php' \
|
||||
'3614ec17912e56dd6439b7acfccffd2c ./public/js/context-js/context-js.js' \
|
||||
'3614ec17912e56dd6439b7acfccffd2c ./resources/js/context-js.js' \
|
||||
'09ba35f39ace53533513f168c739e0ab ./resources/views/user/images.blade.php' \
|
||||
| md5sum -c - \
|
||||
&& grep -q "assetVersion('js/context-js/context-js.js')" ./resources/views/user/images.blade.php \
|
||||
&& grep -q 'isIOSWebKit' ./public/js/context-js/context-js.js \
|
||||
@@ -129,8 +129,8 @@ RUN printf '%s\n' \
|
||||
RUN printf '%s\n' \
|
||||
"fork_sha=${FORK_SHA}" \
|
||||
"lsky_commit=${LSKY_COMMIT}" \
|
||||
"context_js_md5=b916555ab122d2b3db99153d9316a887" \
|
||||
"images_blade_md5=00f38dbf29c3d5cd9800dfd1d6709fb9" \
|
||||
"context_js_md5=3614ec17912e56dd6439b7acfccffd2c" \
|
||||
"images_blade_md5=09ba35f39ace53533513f168c739e0ab" \
|
||||
> .code-revision \
|
||||
&& cat .code-revision
|
||||
|
||||
|
||||
+247
-23
@@ -34,9 +34,9 @@
|
||||
# 桌面端不受影响:仍是 hover 侧开 + fitSubmenu 钳制。
|
||||
#
|
||||
# 三个文件各自的 md5(与 Dockerfile「自证 2」里的期望值一致):
|
||||
# b916555ab122d2b3db99153d9316a887 src/resources/js/context-js.js
|
||||
# b916555ab122d2b3db99153d9316a887 src/public/js/context-js/context-js.js
|
||||
# 00f38dbf29c3d5cd9800dfd1d6709fb9 src/resources/views/user/images.blade.php
|
||||
# 3614ec17912e56dd6439b7acfccffd2c src/resources/js/context-js.js
|
||||
# 3614ec17912e56dd6439b7acfccffd2c src/public/js/context-js/context-js.js
|
||||
# 09ba35f39ace53533513f168c739e0ab src/resources/views/user/images.blade.php
|
||||
#
|
||||
# 上游原文件 md5(改动前,供比对):
|
||||
# c8e57f6232848ca8277341ddf1a3a7a6 resources/js/context-js.js
|
||||
@@ -95,7 +95,7 @@
|
||||
let options = {
|
||||
fadeSpeed: 100,
|
||||
filter: function ($obj) {
|
||||
@@ -15,14 +55,736 @@
|
||||
@@ -15,14 +55,819 @@
|
||||
compress: false
|
||||
};
|
||||
|
||||
@@ -252,6 +252,9 @@
|
||||
+ let touchGestureId = 0; // 每一次「手指按下」算一个新手势
|
||||
+ let armedGestureId = 0; // 二级菜单是在哪一次手势里被打开的
|
||||
+ let submenuArmingTimer = null; // 「刚弹出、先别高亮」的收尾定时器 // 因"点了菜单外"而关菜单后,要吞掉的那一发 click
|
||||
+ let armedMenuGestureId = 0; // 主菜单是在哪一次手势里被打开的
|
||||
+ let menuArmingTimer = null; // 主菜单「刚弹出、先别高亮」的收尾定时器
|
||||
+ let fingerOnScreen = false; // 屏幕上还有没有手指按着(主菜单 arming 窗口据此跟手势续期,而不是死定时器)
|
||||
+ let menuStylesInjected = false;
|
||||
+ let menuGuardInstalled = false;
|
||||
+
|
||||
@@ -285,6 +288,15 @@
|
||||
+ + ' min-width: 0 !important; margin: 0 !important; padding: 0 !important;'
|
||||
+ + ' border: 0 !important; border-radius: 0 !important; box-shadow: none !important;'
|
||||
+ + ' background: transparent !important; }'
|
||||
+ // 主菜单版(与下面二级菜单那份同一套 arming 机制,见 armMenuHoverGuard):
|
||||
+ // 长按弹出的主菜单同样会顶在手指底下,真机触摸的粘滞 hover 会把手指底下那一项当成 hover 目标
|
||||
+ // → 弹出瞬间那项就跳高亮(老师截图里的「复制图片」)。这里连 li 一起关指针事件:
|
||||
+ // 只关 a 的话,主题里的 `.dropdown-submenu:hover > a` 仍会因为 li 被 hover 而给「复制链接」上色。
|
||||
+ // 另外 hover 那套是「底色 + 文字变白」两件一起上(见 context-js.less 的 li>a:hover),
|
||||
+ // 所以文字色也一并还原 —— 万一 :hover/:focus 还是粘住了,那一项也保持原样,不会变成白底白字。
|
||||
+ + '.dropdown-context.menu-arming > li,'
|
||||
+ + ' .dropdown-context.menu-arming > li > a {'
|
||||
+ + ' pointer-events: none !important; background-color: transparent !important; color: inherit !important; }'
|
||||
+ // 刚弹出的一瞬间:项不接受指针事件(浏览器就不会把它当成 hover 目标 → 不会高亮),
|
||||
+ // 同时把 hover 底色压平做双保险;不改变任何尺寸/位置,面板本身照旧接收点击(由守卫吞掉)。
|
||||
+ + '.dropdown-context.submenu-inplace.submenu-arming > li.submenu-active > .dropdown-menu > li > a,'
|
||||
@@ -390,6 +402,9 @@
|
||||
+ const EDGE_MARGIN = 8;
|
||||
+ const SUBMENU_GUARD = 350; // 二级菜单「就地替换」后这段毫秒内,触摸点击一律吞掉(防按住误选)
|
||||
+ const SUBMENU_ARM_MS = 420; // 二级菜单刚弹出的这段毫秒内,先别接受指针指向(防那一下高亮跳色)
|
||||
+ const MENU_ARM_MS = 420; // 主菜单(长按弹出)同理:弹出后这段时间内的项不接受指针指向
|
||||
+ const MENU_ARM_TAIL_MS = 260; // 抬手之后再压这么多毫秒 —— 老师原话「手抬起来的那一瞬间留冗余」,
|
||||
+ // 与二级菜单那份抬手冗余同一个数(见 armMenuHoverGuard / touchend)
|
||||
+
|
||||
+ function viewportSize() {
|
||||
+ const de = document.documentElement;
|
||||
@@ -542,6 +557,50 @@
|
||||
+ $('.dropdown-context').removeClass('submenu-arming');
|
||||
+ }
|
||||
+
|
||||
+ // 主菜单版(同一套机制,老师要求「复用到长按弹出的主菜单」):
|
||||
+ // 主菜单弹在手指底下时,那一项会被浏览器判成 hover 目标 → 弹出瞬间跳高亮,功能和以前一样、就是看着脏。
|
||||
+ // 做法与二级菜单那份一致:挂 .menu-arming(里面的项不接受指针事件 + hover 底色压平),
|
||||
+ // 抬手后由 touchend 再续 MENU_ARM_TAIL_MS —— 窗口跟着手势走,不是弹出时定死的一个定时器。
|
||||
+ // 只在「这一发菜单确实是紧随一次触摸打开」时才挂(判据见 openMenu):桌面右键路径因此一行都不动。
|
||||
+ function armMenuHoverGuard(ms) {
|
||||
+ $('.dropdown-context:not(.dropdown-context-sub)').addClass('menu-arming');
|
||||
+ clearTimeout(menuArmingTimer);
|
||||
+ menuArmingTimer = setTimeout(function () {
|
||||
+ // 手指还按着(而且就是按出这个菜单的那次手势)→ 续期:按住多久都不跳色;
|
||||
+ // 抬手那条路径会再压 MENU_ARM_TAIL_MS 收尾。
|
||||
+ if (fingerOnScreen && armedMenuGestureId && armedMenuGestureId === touchGestureId) {
|
||||
+ armMenuHoverGuard(MENU_ARM_MS);
|
||||
+ return;
|
||||
+ }
|
||||
+ $('.dropdown-context').removeClass('menu-arming');
|
||||
+ }, ms);
|
||||
+ }
|
||||
+
|
||||
+ function clearMenuHoverGuard() {
|
||||
+ clearTimeout(menuArmingTimer);
|
||||
+ $('.dropdown-context').removeClass('menu-arming');
|
||||
+ }
|
||||
+
|
||||
+ // arming 生效时面板里的项不接受指针事件(这正是"不跳高亮"的关键),于是手指按在菜单项上时
|
||||
+ // e.target 拿到的是面板本身 —— 「按一下就展开二级菜单」这条触摸路径会被吃掉。
|
||||
+ // 这里按触摸坐标把手指底下那一行找回来,只给这一个分支用,保证窗口内的第一次点按与平时一致。
|
||||
+ function rowUnderTouch(e) {
|
||||
+ if (! e || ! e.touches || ! e.touches.length) {
|
||||
+ return null;
|
||||
+ }
|
||||
+
|
||||
+ const t = e.touches[0], x = t.clientX, y = t.clientY;
|
||||
+ const rows = document.querySelectorAll('.dropdown-context:not(.dropdown-context-sub) > li');
|
||||
+ for (let i = 0; i < rows.length; i++) {
|
||||
+ const r = rows[i].getBoundingClientRect();
|
||||
+ if (r.width && r.height && x >= r.left && x <= r.right && y >= r.top && y <= r.bottom) {
|
||||
+ return rows[i];
|
||||
+ }
|
||||
+ }
|
||||
+
|
||||
+ return null;
|
||||
+ }
|
||||
+
|
||||
+ // 打开某个父项的二级菜单:手机惯例是「按下就开」,这样抬手那发合成 click 也有东西可吞。
|
||||
+ function openSubmenuFor($li) {
|
||||
+ $li.siblings('.dropdown-submenu').removeClass('touch-open');
|
||||
@@ -729,13 +788,23 @@
|
||||
+ // 等抬手才处理就晚了(那时 click 已经落在菜单项上了)。
|
||||
+ // 抬手:面板若还在「刚弹出」窗口内,把窗口续到抬手之后 —— 按住再久,那一刻也不跳高亮。
|
||||
+ ['touchend', 'touchcancel'].forEach(function (evt) {
|
||||
+ document.addEventListener(evt, function () {
|
||||
+ document.addEventListener(evt, function (e) {
|
||||
+ // 屏幕上还有手指按着吗(多指时抬手一根不算)——主菜单 arming 窗口据此决定要不要续期
|
||||
+ fingerOnScreen = !! (e && e.touches && e.touches.length);
|
||||
+
|
||||
+ // 判据是「这一发手指就是打开二级菜单的那一发」——不能看 .submenu-arming 还在不在:
|
||||
+ // 按住超过 SUBMENU_ARM_MS 时,那个类早就被定时器摘掉了(踩过)。
|
||||
+ if (armedGestureId && armedGestureId === touchGestureId
|
||||
+ && $('.dropdown-context.submenu-inplace').length) {
|
||||
+ armSubmenuHoverGuard(260); // 抬手后再压 260ms,抬手那一刻也不会跳色
|
||||
+ }
|
||||
+
|
||||
+ // 主菜单同理(老师原话:「只要给我手抬起来的那一瞬间留冗余就够」)——
|
||||
+ // 判据同样是「这一发手指就是打开主菜单的那一发」,按住再久也不会在抬手那一刻跳色。
|
||||
+ if (armedMenuGestureId && armedMenuGestureId === touchGestureId
|
||||
+ && $('.dropdown-context:not(.dropdown-context-sub)').length) {
|
||||
+ armMenuHoverGuard(MENU_ARM_TAIL_MS);
|
||||
+ }
|
||||
+ }, true);
|
||||
+ });
|
||||
+
|
||||
@@ -744,7 +813,17 @@
|
||||
+
|
||||
+ // 只看「手指底下这个 a 的直接父级」—— 二级菜单的项嵌在父 li 里面,
|
||||
+ // 用 closest('li.dropdown-submenu') 会把二级菜单里的项也误判成父行。
|
||||
+ const $a = $(e.target).closest('a');
|
||||
+ let $a = $(e.target).closest('a');
|
||||
+ // 主菜单 arming 窗口里,项不接受指针事件 → 手指按在「复制链接」上时 e.target 只是面板,
|
||||
+ // 这一发就按不到那个 a。按坐标把行找回来,只补「按一下就展开二级菜单」这一条路
|
||||
+ // (叶子项靠抬手补发的那发 click,本来就不受影响;就地替换态不兜底,那时的行高/内容都变了)。
|
||||
+ if (! $a.length && $('.dropdown-context.menu-arming').length) {
|
||||
+ const row = rowUnderTouch(e);
|
||||
+ if (row && row.classList.contains('dropdown-submenu')
|
||||
+ && ! row.classList.contains('submenu-active')) {
|
||||
+ $a = $(row).children('a');
|
||||
+ }
|
||||
+ }
|
||||
+ const $row = $a.parent();
|
||||
+ if ($row.hasClass('dropdown-submenu') && $row.closest('.dropdown-context').length) {
|
||||
+ if ($row.hasClass('submenu-active')) {
|
||||
@@ -754,13 +833,17 @@
|
||||
+ }
|
||||
+ submenuInplaceAt = Date.now();
|
||||
+ armedGestureId = touchGestureId; // 武装到本次手势:这根手指抬起来的那发 click 必吞
|
||||
+ } else if ($('.dropdown-context.submenu-arming').length) {
|
||||
+ clearSubmenuHoverGuard(); // 手指挪去点别的东西了:立刻恢复正常 hover/点击
|
||||
+ } else {
|
||||
+ // 手指挪去点别的东西了(包括点菜单项、滑菜单):立刻恢复正常 hover/点击。
|
||||
+ // 两套 arming 都清 —— 它们各自只对「按出它来的那一次手势」负责,新手势一律失效。
|
||||
+ clearSubmenuHoverGuard();
|
||||
+ clearMenuHoverGuard();
|
||||
+ }
|
||||
+ }, true);
|
||||
+
|
||||
+ document.addEventListener('touchstart', function (e) {
|
||||
+ lastTouchAt = Date.now();
|
||||
+ fingerOnScreen = true; // 手指按下:主菜单 arming 窗口可以跟手势续期
|
||||
+
|
||||
+ if (! menuOnScreen() || isInsideMenu(e.target)) {
|
||||
+ return;
|
||||
@@ -835,7 +918,7 @@
|
||||
});
|
||||
if (options.preventDoubleContext) {
|
||||
$(document).on('contextmenu', '.dropdown-context', function (e) {
|
||||
@@ -30,13 +792,8 @@
|
||||
@@ -30,13 +875,8 @@
|
||||
});
|
||||
}
|
||||
$(document).on('mouseenter', '.dropdown-submenu', function () {
|
||||
@@ -851,7 +934,7 @@
|
||||
});
|
||||
|
||||
}
|
||||
@@ -130,12 +887,15 @@
|
||||
@@ -130,12 +970,15 @@
|
||||
let data = opts.data || {};
|
||||
let id = new Date().getTime();
|
||||
|
||||
@@ -872,7 +955,7 @@
|
||||
|
||||
let $menu = buildMenu(item, data, id);
|
||||
// clear dropdowns
|
||||
@@ -144,42 +904,108 @@
|
||||
@@ -144,42 +987,117 @@
|
||||
$('body').append($menu);
|
||||
|
||||
$('.dropdown-context:not(.dropdown-context-sub)').hide();
|
||||
@@ -917,6 +1000,15 @@
|
||||
+ menuOpenedAt = Date.now();
|
||||
+ menuClosingUntil = 0;
|
||||
+
|
||||
+ // 刚弹出的主菜单会顶在手指底下:那一项马上会被当成 hover 目标 → 弹出瞬间就跳高亮。
|
||||
+ // 复用二级菜单那套 arming:挂 .menu-arming(项不接受指针事件 + hover 底色压平),抬手后再续 260ms。
|
||||
+ // **只在「这一发菜单确实紧随一次触摸」时才挂**:鼠标右键路径(lastTouchAt 是 0 或很久以前)
|
||||
+ // 与上游完全一致 —— 桌面本来没有粘滞 hover,不该为此改动付出任何代价。
|
||||
+ if ((Date.now() - lastTouchAt) < MENU_OPEN_GRACE) {
|
||||
+ armedMenuGestureId = touchGestureId;
|
||||
+ armMenuHoverGuard(MENU_ARM_MS);
|
||||
+ }
|
||||
+
|
||||
+ logMenuEvent('open trigger=' + (evt ? 'contextmenu' : 'longpress')
|
||||
+ + ' item=' + describeNode(item)
|
||||
+ + ' at=' + Math.round(pageX) + ',' + Math.round(pageY));
|
||||
@@ -993,7 +1085,7 @@
|
||||
})();
|
||||
--- a/public/js/context-js/context-js.js
|
||||
+++ b/public/js/context-js/context-js.js
|
||||
@@ -1 +1,1011 @@
|
||||
@@ -1 +1,1103 @@
|
||||
-window.context=window.context||function(){let e={fadeSpeed:100,filter:function(e){},above:"auto",preventDoubleContext:!0,compress:!1};function t(o,n,d,i){let a=i?" dropdown-context-sub":"",s=e.compress?" compressed-context":"",f=$('<ul class="dropdown-menu dropdown-context'+a+s+'" id="dropdown-'+d+'"></ul>'),l=0,r="";for(;l<n.length;l++){if(void 0!==n[l].divider)f.append('<li class="divider"></li>');else if(void 0!==n[l].header)f.append('<li class="nav-header">'+n[l].header+"</li>");else{let e;void 0===n[l].href&&(n[l].href="javascript:void(0)"),void 0!==n[l].target&&(r=' target="'+n[l].target+'"'),e=void 0!==n[l].subMenu?$('<li class="dropdown-submenu"><a tabindex="-1" href="'+n[l].href+'">'+n[l].text+"</a></li>"):$('<li><a tabindex="-1" href="'+n[l].href+'"'+r+">"+n[l].text+"</a></li>"),"function"==typeof n[l].visible&&(n[l].visible(o)||e.hide());let i=e.find("a");if(void 0!==n[l].classes)for(const e in n[l].classes)i.addClass(n[l].classes[e]);if(void 0!==n[l].attributes)for(const e in n[l].attributes)i.attr(e,n[l].attributes[e]);if(void 0!==n[l].action){let e="event-"+(new Date).getTime()*Math.floor(1e5*Math.random()),t=n[l].action;i.attr("id",e),$("#"+e).addClass("context-event"),$(document).on("click","#"+e,(function(){t.call(this,o)}))}if(f.append(e),void 0!==n[l].subMenu){let e=t(o,n[l].subMenu,d,!0);f.find("li:last").append(e)}}"function"==typeof e.filter&&e.filter(f.find("li:last"))}return f}return{init:function(t){e=$.extend({},e,t),$(document).on("click","html",(function(){$(".dropdown-context").fadeOut(e.fadeSpeed,(function(){$(".dropdown-context").css({display:""}).find(".drop-left").removeClass("drop-left")}))})),e.preventDoubleContext&&$(document).on("contextmenu",".dropdown-context",(function(e){e.preventDefault()})),$(document).on("mouseenter",".dropdown-submenu",(function(){let e=$(this).find(".dropdown-context-sub:first");e.width()+e.offset().left>window.innerWidth&&e.addClass("drop-left")}))},settings:function(t){e=$.extend({},e,t)},attach:function(o,n){let d=(n=n||{}).data||{},i=(new Date).getTime();$(document).on("contextmenu",o,(function(a){a.preventDefault(),a.stopPropagation();let s=a.target.closest(o);"function"==typeof n.beforeOpen&&n.beforeOpen.call(a,s);let f=t(s,d,i);$("body .dropdown-menu.dropdown-context").remove(),$("body").append(f),$(".dropdown-context:not(.dropdown-context-sub)").hide();let l=$("#dropdown-"+i);if("boolean"==typeof e.above&&e.above)l.addClass("dropdown-context-up").css({top:a.pageY-20-$("#dropdown-"+i).height(),left:a.pageX-13}).fadeIn(e.fadeSpeed);else if("string"==typeof e.above&&"auto"===e.above){l.removeClass("dropdown-context-up");let t=l.height()+12;a.pageY+t>$("html").height()?l.addClass("dropdown-context-up").css({top:a.pageY-20-t,left:a.pageX-13}).fadeIn(e.fadeSpeed):l.css({top:a.pageY+10,left:a.pageX-13}).fadeIn(e.fadeSpeed)}"function"==typeof n.afterOpen&&n.afterOpen.call(a,s,l.get(0))}))},destroy:function(e){$(document).off("contextmenu",e).off("click",".context-event")}}}();
|
||||
+/*
|
||||
+ * Context.js
|
||||
@@ -1205,6 +1297,9 @@
|
||||
+ let touchGestureId = 0; // 每一次「手指按下」算一个新手势
|
||||
+ let armedGestureId = 0; // 二级菜单是在哪一次手势里被打开的
|
||||
+ let submenuArmingTimer = null; // 「刚弹出、先别高亮」的收尾定时器 // 因"点了菜单外"而关菜单后,要吞掉的那一发 click
|
||||
+ let armedMenuGestureId = 0; // 主菜单是在哪一次手势里被打开的
|
||||
+ let menuArmingTimer = null; // 主菜单「刚弹出、先别高亮」的收尾定时器
|
||||
+ let fingerOnScreen = false; // 屏幕上还有没有手指按着(主菜单 arming 窗口据此跟手势续期,而不是死定时器)
|
||||
+ let menuStylesInjected = false;
|
||||
+ let menuGuardInstalled = false;
|
||||
+
|
||||
@@ -1238,6 +1333,15 @@
|
||||
+ + ' min-width: 0 !important; margin: 0 !important; padding: 0 !important;'
|
||||
+ + ' border: 0 !important; border-radius: 0 !important; box-shadow: none !important;'
|
||||
+ + ' background: transparent !important; }'
|
||||
+ // 主菜单版(与下面二级菜单那份同一套 arming 机制,见 armMenuHoverGuard):
|
||||
+ // 长按弹出的主菜单同样会顶在手指底下,真机触摸的粘滞 hover 会把手指底下那一项当成 hover 目标
|
||||
+ // → 弹出瞬间那项就跳高亮(老师截图里的「复制图片」)。这里连 li 一起关指针事件:
|
||||
+ // 只关 a 的话,主题里的 `.dropdown-submenu:hover > a` 仍会因为 li 被 hover 而给「复制链接」上色。
|
||||
+ // 另外 hover 那套是「底色 + 文字变白」两件一起上(见 context-js.less 的 li>a:hover),
|
||||
+ // 所以文字色也一并还原 —— 万一 :hover/:focus 还是粘住了,那一项也保持原样,不会变成白底白字。
|
||||
+ + '.dropdown-context.menu-arming > li,'
|
||||
+ + ' .dropdown-context.menu-arming > li > a {'
|
||||
+ + ' pointer-events: none !important; background-color: transparent !important; color: inherit !important; }'
|
||||
+ // 刚弹出的一瞬间:项不接受指针事件(浏览器就不会把它当成 hover 目标 → 不会高亮),
|
||||
+ // 同时把 hover 底色压平做双保险;不改变任何尺寸/位置,面板本身照旧接收点击(由守卫吞掉)。
|
||||
+ + '.dropdown-context.submenu-inplace.submenu-arming > li.submenu-active > .dropdown-menu > li > a,'
|
||||
@@ -1343,6 +1447,9 @@
|
||||
+ const EDGE_MARGIN = 8;
|
||||
+ const SUBMENU_GUARD = 350; // 二级菜单「就地替换」后这段毫秒内,触摸点击一律吞掉(防按住误选)
|
||||
+ const SUBMENU_ARM_MS = 420; // 二级菜单刚弹出的这段毫秒内,先别接受指针指向(防那一下高亮跳色)
|
||||
+ const MENU_ARM_MS = 420; // 主菜单(长按弹出)同理:弹出后这段时间内的项不接受指针指向
|
||||
+ const MENU_ARM_TAIL_MS = 260; // 抬手之后再压这么多毫秒 —— 老师原话「手抬起来的那一瞬间留冗余」,
|
||||
+ // 与二级菜单那份抬手冗余同一个数(见 armMenuHoverGuard / touchend)
|
||||
+
|
||||
+ function viewportSize() {
|
||||
+ const de = document.documentElement;
|
||||
@@ -1495,6 +1602,50 @@
|
||||
+ $('.dropdown-context').removeClass('submenu-arming');
|
||||
+ }
|
||||
+
|
||||
+ // 主菜单版(同一套机制,老师要求「复用到长按弹出的主菜单」):
|
||||
+ // 主菜单弹在手指底下时,那一项会被浏览器判成 hover 目标 → 弹出瞬间跳高亮,功能和以前一样、就是看着脏。
|
||||
+ // 做法与二级菜单那份一致:挂 .menu-arming(里面的项不接受指针事件 + hover 底色压平),
|
||||
+ // 抬手后由 touchend 再续 MENU_ARM_TAIL_MS —— 窗口跟着手势走,不是弹出时定死的一个定时器。
|
||||
+ // 只在「这一发菜单确实是紧随一次触摸打开」时才挂(判据见 openMenu):桌面右键路径因此一行都不动。
|
||||
+ function armMenuHoverGuard(ms) {
|
||||
+ $('.dropdown-context:not(.dropdown-context-sub)').addClass('menu-arming');
|
||||
+ clearTimeout(menuArmingTimer);
|
||||
+ menuArmingTimer = setTimeout(function () {
|
||||
+ // 手指还按着(而且就是按出这个菜单的那次手势)→ 续期:按住多久都不跳色;
|
||||
+ // 抬手那条路径会再压 MENU_ARM_TAIL_MS 收尾。
|
||||
+ if (fingerOnScreen && armedMenuGestureId && armedMenuGestureId === touchGestureId) {
|
||||
+ armMenuHoverGuard(MENU_ARM_MS);
|
||||
+ return;
|
||||
+ }
|
||||
+ $('.dropdown-context').removeClass('menu-arming');
|
||||
+ }, ms);
|
||||
+ }
|
||||
+
|
||||
+ function clearMenuHoverGuard() {
|
||||
+ clearTimeout(menuArmingTimer);
|
||||
+ $('.dropdown-context').removeClass('menu-arming');
|
||||
+ }
|
||||
+
|
||||
+ // arming 生效时面板里的项不接受指针事件(这正是"不跳高亮"的关键),于是手指按在菜单项上时
|
||||
+ // e.target 拿到的是面板本身 —— 「按一下就展开二级菜单」这条触摸路径会被吃掉。
|
||||
+ // 这里按触摸坐标把手指底下那一行找回来,只给这一个分支用,保证窗口内的第一次点按与平时一致。
|
||||
+ function rowUnderTouch(e) {
|
||||
+ if (! e || ! e.touches || ! e.touches.length) {
|
||||
+ return null;
|
||||
+ }
|
||||
+
|
||||
+ const t = e.touches[0], x = t.clientX, y = t.clientY;
|
||||
+ const rows = document.querySelectorAll('.dropdown-context:not(.dropdown-context-sub) > li');
|
||||
+ for (let i = 0; i < rows.length; i++) {
|
||||
+ const r = rows[i].getBoundingClientRect();
|
||||
+ if (r.width && r.height && x >= r.left && x <= r.right && y >= r.top && y <= r.bottom) {
|
||||
+ return rows[i];
|
||||
+ }
|
||||
+ }
|
||||
+
|
||||
+ return null;
|
||||
+ }
|
||||
+
|
||||
+ // 打开某个父项的二级菜单:手机惯例是「按下就开」,这样抬手那发合成 click 也有东西可吞。
|
||||
+ function openSubmenuFor($li) {
|
||||
+ $li.siblings('.dropdown-submenu').removeClass('touch-open');
|
||||
@@ -1682,13 +1833,23 @@
|
||||
+ // 等抬手才处理就晚了(那时 click 已经落在菜单项上了)。
|
||||
+ // 抬手:面板若还在「刚弹出」窗口内,把窗口续到抬手之后 —— 按住再久,那一刻也不跳高亮。
|
||||
+ ['touchend', 'touchcancel'].forEach(function (evt) {
|
||||
+ document.addEventListener(evt, function () {
|
||||
+ document.addEventListener(evt, function (e) {
|
||||
+ // 屏幕上还有手指按着吗(多指时抬手一根不算)——主菜单 arming 窗口据此决定要不要续期
|
||||
+ fingerOnScreen = !! (e && e.touches && e.touches.length);
|
||||
+
|
||||
+ // 判据是「这一发手指就是打开二级菜单的那一发」——不能看 .submenu-arming 还在不在:
|
||||
+ // 按住超过 SUBMENU_ARM_MS 时,那个类早就被定时器摘掉了(踩过)。
|
||||
+ if (armedGestureId && armedGestureId === touchGestureId
|
||||
+ && $('.dropdown-context.submenu-inplace').length) {
|
||||
+ armSubmenuHoverGuard(260); // 抬手后再压 260ms,抬手那一刻也不会跳色
|
||||
+ }
|
||||
+
|
||||
+ // 主菜单同理(老师原话:「只要给我手抬起来的那一瞬间留冗余就够」)——
|
||||
+ // 判据同样是「这一发手指就是打开主菜单的那一发」,按住再久也不会在抬手那一刻跳色。
|
||||
+ if (armedMenuGestureId && armedMenuGestureId === touchGestureId
|
||||
+ && $('.dropdown-context:not(.dropdown-context-sub)').length) {
|
||||
+ armMenuHoverGuard(MENU_ARM_TAIL_MS);
|
||||
+ }
|
||||
+ }, true);
|
||||
+ });
|
||||
+
|
||||
@@ -1697,7 +1858,17 @@
|
||||
+
|
||||
+ // 只看「手指底下这个 a 的直接父级」—— 二级菜单的项嵌在父 li 里面,
|
||||
+ // 用 closest('li.dropdown-submenu') 会把二级菜单里的项也误判成父行。
|
||||
+ const $a = $(e.target).closest('a');
|
||||
+ let $a = $(e.target).closest('a');
|
||||
+ // 主菜单 arming 窗口里,项不接受指针事件 → 手指按在「复制链接」上时 e.target 只是面板,
|
||||
+ // 这一发就按不到那个 a。按坐标把行找回来,只补「按一下就展开二级菜单」这一条路
|
||||
+ // (叶子项靠抬手补发的那发 click,本来就不受影响;就地替换态不兜底,那时的行高/内容都变了)。
|
||||
+ if (! $a.length && $('.dropdown-context.menu-arming').length) {
|
||||
+ const row = rowUnderTouch(e);
|
||||
+ if (row && row.classList.contains('dropdown-submenu')
|
||||
+ && ! row.classList.contains('submenu-active')) {
|
||||
+ $a = $(row).children('a');
|
||||
+ }
|
||||
+ }
|
||||
+ const $row = $a.parent();
|
||||
+ if ($row.hasClass('dropdown-submenu') && $row.closest('.dropdown-context').length) {
|
||||
+ if ($row.hasClass('submenu-active')) {
|
||||
@@ -1707,13 +1878,17 @@
|
||||
+ }
|
||||
+ submenuInplaceAt = Date.now();
|
||||
+ armedGestureId = touchGestureId; // 武装到本次手势:这根手指抬起来的那发 click 必吞
|
||||
+ } else if ($('.dropdown-context.submenu-arming').length) {
|
||||
+ clearSubmenuHoverGuard(); // 手指挪去点别的东西了:立刻恢复正常 hover/点击
|
||||
+ } else {
|
||||
+ // 手指挪去点别的东西了(包括点菜单项、滑菜单):立刻恢复正常 hover/点击。
|
||||
+ // 两套 arming 都清 —— 它们各自只对「按出它来的那一次手势」负责,新手势一律失效。
|
||||
+ clearSubmenuHoverGuard();
|
||||
+ clearMenuHoverGuard();
|
||||
+ }
|
||||
+ }, true);
|
||||
+
|
||||
+ document.addEventListener('touchstart', function (e) {
|
||||
+ lastTouchAt = Date.now();
|
||||
+ fingerOnScreen = true; // 手指按下:主菜单 arming 窗口可以跟手势续期
|
||||
+
|
||||
+ if (! menuOnScreen() || isInsideMenu(e.target)) {
|
||||
+ return;
|
||||
@@ -1934,6 +2109,15 @@
|
||||
+ menuOpenedAt = Date.now();
|
||||
+ menuClosingUntil = 0;
|
||||
+
|
||||
+ // 刚弹出的主菜单会顶在手指底下:那一项马上会被当成 hover 目标 → 弹出瞬间就跳高亮。
|
||||
+ // 复用二级菜单那套 arming:挂 .menu-arming(项不接受指针事件 + hover 底色压平),抬手后再续 260ms。
|
||||
+ // **只在「这一发菜单确实紧随一次触摸」时才挂**:鼠标右键路径(lastTouchAt 是 0 或很久以前)
|
||||
+ // 与上游完全一致 —— 桌面本来没有粘滞 hover,不该为此改动付出任何代价。
|
||||
+ if ((Date.now() - lastTouchAt) < MENU_OPEN_GRACE) {
|
||||
+ armedMenuGestureId = touchGestureId;
|
||||
+ armMenuHoverGuard(MENU_ARM_MS);
|
||||
+ }
|
||||
+
|
||||
+ logMenuEvent('open trigger=' + (evt ? 'contextmenu' : 'longpress')
|
||||
+ + ' item=' + describeNode(item)
|
||||
+ + ' at=' + Math.round(pageX) + ',' + Math.round(pageY));
|
||||
@@ -2385,7 +2569,20 @@
|
||||
}
|
||||
|
||||
html += item;
|
||||
@@ -486,10 +569,10 @@
|
||||
@@ -386,6 +469,12 @@
|
||||
$albums.off('click', '.delete').on('click', '.delete', function (e) {
|
||||
e.stopPropagation();
|
||||
Swal.fire({
|
||||
+ // 与新弹窗(x-modal)一套:不动背景页面。
|
||||
+ // heightAuto 关掉 html/body 上的 swal2-height-auto;scrollbarPadding 关掉
|
||||
+ // sweetalert2 往 body 写 padding-right 的那一步(它按「滚动条宽度」补内边距,
|
||||
+ // 手机上明明没有占位滚动条,补 8px 却真的会把内容挤窄 → 工具栏换行、整页下移)。
|
||||
+ heightAuto: false,
|
||||
+ scrollbarPadding: false,
|
||||
title: '确认删除该相册?',
|
||||
text: "删除后相册中的图片将会被移出。",
|
||||
icon: 'warning',
|
||||
@@ -486,10 +575,10 @@
|
||||
operates = ['refresh'];
|
||||
}
|
||||
if (selected.length === 1) {
|
||||
@@ -2398,7 +2595,7 @@
|
||||
}
|
||||
if (selected.length && selectedAlbum.id !== undefined) {
|
||||
operates.push('remove');
|
||||
@@ -509,6 +592,26 @@
|
||||
@@ -509,6 +598,26 @@
|
||||
ds.subscribe('elementselect', _ => bindOperates());
|
||||
ds.subscribe('elementunselect', _ => bindOperates());
|
||||
|
||||
@@ -2425,7 +2622,7 @@
|
||||
$photos.on('click', '.image-selector', function () {
|
||||
ds.toggleSelection($(this).closest('a'));
|
||||
bindOperates();
|
||||
@@ -535,25 +638,94 @@
|
||||
@@ -535,25 +644,94 @@
|
||||
|
||||
const methods = {
|
||||
movements() {
|
||||
@@ -2537,16 +2734,43 @@
|
||||
},
|
||||
remove() {
|
||||
let selected = ds.getSelection().map(item => $(item).data('id'));
|
||||
@@ -575,7 +747,7 @@
|
||||
@@ -574,8 +752,12 @@
|
||||
},
|
||||
permission() {
|
||||
Swal.fire({
|
||||
+ // 与新弹窗(x-modal)一套:不动背景页面(heightAuto 去 swal2-height-auto,
|
||||
+ // scrollbarPadding 去 body 的 padding-right 注入 —— 否则整页会有细微位移)
|
||||
+ heightAuto: false,
|
||||
+ scrollbarPadding: false,
|
||||
title: '选择一个权限',
|
||||
- text: '选择公开将会出现在画廊中(若平台开启了画廊)',
|
||||
+ text: '选择公开后所有用户都能看到这张图片',
|
||||
input: 'select',
|
||||
inputOptions: {
|
||||
public: '公开',
|
||||
@@ -684,6 +856,7 @@
|
||||
@@ -613,6 +795,10 @@
|
||||
rename(e) {
|
||||
let item = $(e).data('json');
|
||||
Swal.fire({
|
||||
+ // 与新弹窗(x-modal)一套:不动背景页面(heightAuto 去 swal2-height-auto,
|
||||
+ // scrollbarPadding 去 body 的 padding-right 注入 —— 否则整页会有细微位移)
|
||||
+ heightAuto: false,
|
||||
+ scrollbarPadding: false,
|
||||
title: '请输入图片名称',
|
||||
input: 'text',
|
||||
inputValue: item.filename,
|
||||
@@ -649,6 +835,10 @@
|
||||
},
|
||||
delete() {
|
||||
Swal.fire({
|
||||
+ // 与新弹窗(x-modal)一套:不动背景页面(heightAuto 去 swal2-height-auto,
|
||||
+ // scrollbarPadding 去 body 的 padding-right 注入 —— 否则整页会有细微位移)
|
||||
+ heightAuto: false,
|
||||
+ scrollbarPadding: false,
|
||||
title: '确认要删除选中的图片?',
|
||||
text: "删除后不可恢复,记录和文件同时删除",
|
||||
icon: 'warning',
|
||||
@@ -684,6 +874,7 @@
|
||||
if (response.data.status) {
|
||||
let image = response.data.data.image;
|
||||
let content = $('#image-detail-tpl').html()
|
||||
@@ -2554,7 +2778,7 @@
|
||||
.replace(/__album_name__/g, image.album ? image.album.name : '-')
|
||||
.replace(/__strategy_name__/g, image.strategy ? image.strategy.name : '-')
|
||||
.replace(/__filename__/g, image.filename.replace(/\$/g, '$$$$'))
|
||||
@@ -697,7 +870,9 @@
|
||||
@@ -697,7 +888,9 @@
|
||||
.replace(/__permission__/g, image.permission === 1 ? '公开' : '私有')
|
||||
.replace(/__uploaded_ip__/g, image.uploaded_ip)
|
||||
.replace(/__created_at__/g, image.created_at)
|
||||
@@ -2565,7 +2789,7 @@
|
||||
} else {
|
||||
toastr.error(response.data.message);
|
||||
}
|
||||
@@ -853,6 +1028,10 @@
|
||||
@@ -853,6 +1046,10 @@
|
||||
case 'delete': // 删除
|
||||
methods.delete();
|
||||
break;
|
||||
|
||||
@@ -1 +1 @@
|
||||
/*! tailwindcss v3.0.23 | MIT License | https://tailwindcss.com*/*,:after,:before{border:0 solid #e5e7eb;box-sizing:border-box}:after,:before{--tw-content:""}html{-webkit-text-size-adjust:100%;font-family:system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,PingFang SC,Hiragino Sans GB,Microsoft YaHei,Noto Sans SC,ui-sans-serif,Roboto,Helvetica Neue,Arial,Noto Sans,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji;line-height:1.5;-moz-tab-size:4;-o-tab-size:4;tab-size:4}body{line-height:inherit;margin:0}hr{border-top-width:1px;color:inherit;height:0}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,pre,samp{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}table{border-collapse:collapse;border-color:inherit;text-indent:0}button,input,optgroup,select,textarea{color:inherit;font-family:inherit;font-size:100%;line-height:inherit;margin:0;padding:0}button,select{text-transform:none}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button;background-color:transparent;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:baseline}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dd,dl,figure,h1,h2,h3,h4,h5,h6,hr,p,pre{margin:0}fieldset{margin:0}fieldset,legend{padding:0}menu,ol,ul{list-style:none;margin:0;padding:0}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{color:#9ca3af;opacity:1}input::placeholder,textarea::placeholder{color:#9ca3af;opacity:1}[role=button],button{cursor:pointer}:disabled{cursor:default}audio,canvas,embed,iframe,img,object,svg,video{display:block;vertical-align:middle}img,video{height:auto;max-width:100%}[hidden]{display:none}[multiple],[type=date],[type=datetime-local],[type=email],[type=month],[type=number],[type=password],[type=search],[type=tel],[type=text],[type=time],[type=url],[type=week],select,textarea{--tw-shadow:0 0 #0000;-webkit-appearance:none;-moz-appearance:none;appearance:none;background-color:#fff;border-color:#6b7280;border-radius:0;border-width:1px;font-size:1rem;line-height:1.5rem;padding:.5rem .75rem}[multiple]:focus,[type=date]:focus,[type=datetime-local]:focus,[type=email]:focus,[type=month]:focus,[type=number]:focus,[type=password]:focus,[type=search]:focus,[type=tel]:focus,[type=text]:focus,[type=time]:focus,[type=url]:focus,[type=week]:focus,select:focus,textarea:focus{--tw-ring-inset:var(--tw-empty,/*!*/ /*!*/);--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:#2563eb;--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color);border-color:#2563eb;box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow);outline:2px solid transparent;outline-offset:2px}input::-moz-placeholder,textarea::-moz-placeholder{color:#6b7280;opacity:1}input::placeholder,textarea::placeholder{color:#6b7280;opacity:1}::-webkit-datetime-edit-fields-wrapper{padding:0}::-webkit-date-and-time-value{min-height:1.5em}::-webkit-datetime-edit,::-webkit-datetime-edit-day-field,::-webkit-datetime-edit-hour-field,::-webkit-datetime-edit-meridiem-field,::-webkit-datetime-edit-millisecond-field,::-webkit-datetime-edit-minute-field,::-webkit-datetime-edit-month-field,::-webkit-datetime-edit-second-field,::-webkit-datetime-edit-year-field{padding-bottom:0;padding-top:0}select{-webkit-print-color-adjust:exact;background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 20 20'%3E%3Cpath stroke='%236b7280' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' d='m6 8 4 4 4-4'/%3E%3C/svg%3E");background-position:right .5rem center;background-repeat:no-repeat;background-size:1.5em 1.5em;color-adjust:exact;padding-right:2.5rem}[multiple]{-webkit-print-color-adjust:unset;background-image:none;background-position:0 0;background-repeat:unset;background-size:initial;color-adjust:unset;padding-right:.75rem}[type=checkbox],[type=radio]{-webkit-print-color-adjust:exact;--tw-shadow:0 0 #0000;-webkit-appearance:none;-moz-appearance:none;appearance:none;background-color:#fff;background-origin:border-box;border-color:#6b7280;border-width:1px;color:#2563eb;color-adjust:exact;display:inline-block;flex-shrink:0;height:1rem;padding:0;-webkit-usLine truncated
|
||||
/*! tailwindcss v3.0.23 | MIT License | https://tailwindcss.com*/*,:after,:before{border:0 solid #e5e7eb;box-sizing:border-box}:after,:before{--tw-content:""}html{-webkit-text-size-adjust:100%;font-family:system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,PingFang SC,Hiragino Sans GB,Microsoft YaHei,Noto Sans SC,ui-sans-serif,Roboto,Helvetica Neue,Arial,Noto Sans,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji;line-height:1.5;-moz-tab-size:4;-o-tab-size:4;tab-size:4}body{line-height:inherit;margin:0}hr{border-top-width:1px;color:inherit;height:0}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,pre,samp{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}table{border-collapse:collapse;border-color:inherit;text-indent:0}button,input,optgroup,select,textarea{color:inherit;font-family:inherit;font-size:100%;line-height:inherit;margin:0;padding:0}button,select{text-transform:none}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button;background-color:transparent;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:baseline}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dd,dl,figure,h1,h2,h3,h4,h5,h6,hr,p,pre{margin:0}fieldset{margin:0}fieldset,legend{padding:0}menu,ol,ul{list-style:none;margin:0;padding:0}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{color:#9ca3af;opacity:1}input::placeholder,textarea::placeholder{color:#9ca3af;opacity:1}[role=button],button{cursor:pointer}:disabled{cursor:default}audio,canvas,embed,iframe,img,object,svg,video{display:block;vertical-align:middle}img,video{height:auto;max-width:100%}[hidden]{display:none}[multiple],[type=date],[type=datetime-local],[type=email],[type=month],[type=number],[type=password],[type=search],[type=tel],[type=text],[type=time],[type=url],[type=week],select,textarea{--tw-shadow:0 0 #0000;-webkit-appearance:none;-moz-appearance:none;appearance:none;background-color:#fff;border-color:#6b7280;border-radius:0;border-width:1px;font-size:1rem;line-height:1.5rem;padding:.5rem .75rem}[multiple]:focus,[type=date]:focus,[type=datetime-local]:focus,[type=email]:focus,[type=month]:focus,[type=number]:focus,[type=password]:focus,[type=search]:focus,[type=tel]:focus,[type=text]:focus,[type=time]:focus,[type=url]:focus,[type=week]:focus,select:focus,textarea:focus{--tw-ring-inset:var(--tw-empty,/*!*/ /*!*/);--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:#2563eb;--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color);border-color:#2563eb;box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow);outline:2px solid transparent;outline-offset:2px}input::-moz-placeholder,textarea::-moz-placeholder{color:#6b7280;opacity:1}input::placeholder,textarea::placeholder{color:#6b7280;opacity:1}::-webkit-datetime-edit-fields-wrapper{padding:0}::-webkit-date-and-time-value{min-height:1.5em}::-webkit-datetime-edit,::-webkit-datetime-edit-day-field,::-webkit-datetime-edit-hour-field,::-webkit-datetime-edit-meridiem-field,::-webkit-datetime-edit-millisecond-field,::-webkit-datetime-edit-minute-field,::-webkit-datetime-edit-month-field,::-webkit-datetime-edit-second-field,::-webkit-datetime-edit-year-field{padding-bottom:0;padding-top:0}select{-webkit-print-color-adjust:exact;background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 20 20'%3E%3Cpath stroke='%236b7280' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' d='m6 8 4 4 4-4'/%3E%3C/svg%3E");background-position:right .5rem center;background-repeat:no-repeat;background-size:1.5em 1.5em;color-adjust:exact;padding-right:2.5rem}[multiple]{-webkit-print-color-adjust:unset;background-image:none;background-position:0 0;background-repeat:unset;background-size:initial;color-adjust:unset;padding-right:.75rem}[type=checkbox],[type=radio]{-webkit-print-color-adjust:exact;--tw-shadow:0 0 #0000;-webkit-appearance:none;-moz-appearance:none;appearance:none;background-color:#fff;background-origin:border-box;border-color:#6b7280;border-width:1px;color:#2563eb;color-adjust:exact;display:inline-block;flex-shrink:0;height:1rem;padding:0;-webkit-usLine truncated
|
||||
@@ -1 +1 @@
|
||||
.toast-title{font-weight:700}.toast-message{-ms-word-wrap:break-word;word-wrap:break-word}.toast-message a,.toast-message label{color:#fff}.toast-message a:hover{color:#ccc;text-decoration:none}.toast-close-button{color:#fff;-ms-filter:progid:DXImageTransform.Microsoft.Alpha(Opacity=80);filter:alpha(opacity=80);float:right;font-size:20px;font-weight:700;line-height:1;opacity:.8;position:relative;right:-.3em;-webkit-text-shadow:0 1px 0 #fff;text-shadow:0 1px 0 #fff;top:-.3em}.toast-close-button:focus,.toast-close-button:hover{color:#000;cursor:pointer;-ms-filter:progid:DXImageTransform.Microsoft.Alpha(Opacity=40);filter:alpha(opacity=40);opacity:.4;text-decoration:none}.rtl .toast-close-button{float:left;left:-.3em;right:.3em}button.toast-close-button{-webkit-appearance:none;background:0 0;border:0;cursor:pointer;padding:0}.toast-top-center{right:0;top:0;width:100%}.toast-bottom-center{bottom:0;right:0;width:100%}.toast-top-full-width{right:0;top:0;width:100%}.toast-bottom-full-width{bottom:0;right:0;width:100%}.toast-top-left{left:12px;top:12px}.toast-top-right{right:12px;top:12px}.toast-bottom-right{bottom:12px;right:12px}.toast-bottom-left{bottom:12px;left:12px}#toast-container{pointer-events:none;position:fixed;z-index:999999}#toast-container *{box-sizing:border-box}#toast-container>div{background-position:15px;background-repeat:no-repeat;border-radius:3px;box-shadow:0 0 12px #999;color:#fff;-ms-filter:progid:DXImageTransform.Microsoft.Alpha(Opacity=80);filter:alpha(opacity=80);margin:0 0 6px;opacity:.8;overflow:hidden;padding:15px 15px 15px 50px;pointer-events:auto;position:relative;width:300px}#toast-container>div.rtl{background-position:right 15px center;direction:rtl;padding:15px 50px 15px 15px}#toast-container>div:hover{box-shadow:0 0 12px #000;cursor:pointer;-ms-filter:progid:DXImageTransform.Microsoft.Alpha(Opacity=100);filter:alpha(opacity=100);opacity:1}#toast-container>.toast-info{background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAGwSURBVEhLtZa9SgNBEMc9sUxxRcoUKSzSWIhXpFMhhYWFhaBg4yPYiWCXZxBLERsLRS3EQkEfwCKdjWJAwSKCgoKCcudv4O5YLrt7EzgXhiU3/4+b2ckmwVjJSpKkQ6wAi4gwhT+z3wRBcEz0yjSseUTrcRyfsHsXmD0AmbHOC9Ii8VImnuXBPglHpQ5wwSVM7sNnTG7Za4JwDdCjxyAiH3nyA2mtaTJufiDZ5dCaqlItILh1NHatfN5skvjx9Z38m69CgzuXmZgVrPIGE763Jx9qKsRozWYw6xOHdER+nn2KkO+Bb+UV5CBN6WC6QtBgbRVozrahAbmm6HtUsgtPC19tFdxXZYBOfkbmFJ1VaHA1VAHjd0pp70oTZzvR+EVrx2Ygfdsq6eu55BHYR8hlcki+n+kERUFG8BrA0BwjeAv2M8WLQBtcy+SD6fNsmnB3AlBLrgTtVW1c2QN4bVWLATaIS60J2Du5y1TiJgjSBvFVZgTmwCU+dAZFoPxGEEs8nyHC9Bwe2GvEJv2WXZb0vjdyFT4Cxk3e/kIqlOGoVLwwPevpYHT+00T+hWwXDf4AJAOUqWcDhbwAAAAASUVORK5CYII=)!important}#toast-container>.toast-error{background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAHOSURBVEhLrZa/SgNBEMZzh0WKCClSCKaIYOED+AAKeQQLG8HWztLCImBrYadgIdY+gIKNYkBFSwu7CAoqCgkkoGBI/E28PdbLZmeDLgzZzcx83/zZ2SSXC1j9fr+I1Hq93g2yxH4iwM1vkoBWAdxCmpzTxfkN2RcyZNaHFIkSo10+8kgxkXIURV5HGxTmFuc75B2RfQkpxHG8aAgaAFa0tAHqYFfQ7Iwe2yhODk8+J4C7yAoRTWI3w/4klGRgR4lO7Rpn9+gvMyWp+uxFh8+H+ARlgN1nJuJuQAYvNkEnwGFck18Er4q3egEc/oO+mhLdKgRyhdNFiacC0rlOCbhNVz4H9FnAYgDBvU3QIioZlJFLJtsoHYRDfiZoUyIxqCtRpVlANq0EU4dApjrtgezPFad5S19Wgjkc0hNVnuF4HjVA6C7QrSIbylB+oZe3aHgBsqlNqKYH48jXyJKMuAbiyVJ8KzaB3eRc0pg9VwQ4niFryI68qiOi3AbjwdsfnAtk0bCjTLJKr6mrD9g8iq/S/B81hguOMlQTnVyG40wAcjnmgsCNESDrjme7wfftP4P7SP4N3CJZdvzoNyGq2c/HWOXJGsvVg+RA/k2MC/wN6I2YA2Pt8GkAAAAASUVORK5CYII=)!important}#toast-container>.toast-success{background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAADsSURBVEhLY2AYBfQMgf///3P8+/evAIgvA/FsIF+BavYDDWMBGroaSMMBiE8VC7AZDrIFaMFnii3AZTjUgsUUWUDA8OdAH6iQbQEhw4HyGsPEcKBXBIC4ARhex4G4BsjmweU1soIFaGg/WtoFZRIZdEvIMhxkCCjXIVsATV6gFGACs4Rsw0EGgIIH3QJYJgHSARQZDrWAB+jawzgs+Q2UO49D7jnRSRGoEFRILcdmEMWGI0cm0JJ2QpYA1RDvcmzJEWhABhD/pqrL0S0CWuABKgnRki9lLseS7g2AlqwHWQSKH4oKLrILpRGhEQCw2LiRUIa4lwAAAABJRU5ErkJggg==)!important}#toast-container>.toast-warning{background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAGYSURBVEhL5ZSvTsNQFMbXZGICMYGYmJhAQIJAICYQPAACiSDB8AiICQQJT4CqQEwgJvYASAQCiZiYmJhAIBATCARJy+9rTsldd8sKu1M0+dLb057v6/lbq/2rK0mS/TRNj9cWNAKPYIJII7gIxCcQ51cvqID+GIEX8ASG4B1bK5gIZFeQfoJdEXOfgX4QAQg7kH2A65yQ87lyxb27sggkAzAuFhbbg1K2kgCkB1bVwyIR9m2L7PRPIhDUIXgGtyKw575yz3lTNs6X4JXnjV+LKM/m3MydnTbtOKIjtz6VhCBq4vSm3ncdrD2lk0VgUXSVKjVDJXJzijW1RQdsU7F77He8u68koNZTz8Oz5yGa6J3H3lZ0xYgXBK2QymlWWA+RWnYhskLBv2vmE+hBMCtbA7KX5drWyRT/2JsqZ2IvfB9Y4bWDNMFbJRFmC9E74SoS0CqulwjkC0+5bpcV1CZ8NMej4pjy0U+doDQsGyo1hzVJttIjhQ7GnBtRFN1UarUlH8F3xict+HY07rEzoUGPlWcjRFRr4/gChZgc3ZL2d8oAAAAASUVORK5CYII=)!important}#toast-container.toast-bottom-center>div,#toast-container.toast-top-center>div{margin-left:auto;margin-right:auto;width:300px}#toaLine truncated
|
||||
.toast-title{font-weight:700}.toast-message{-ms-word-wrap:break-word;word-wrap:break-word}.toast-message a,.toast-message label{color:#fff}.toast-message a:hover{color:#ccc;text-decoration:none}.toast-close-button{color:#fff;-ms-filter:progid:DXImageTransform.Microsoft.Alpha(Opacity=80);filter:alpha(opacity=80);float:right;font-size:20px;font-weight:700;line-height:1;opacity:.8;position:relative;right:-.3em;-webkit-text-shadow:0 1px 0 #fff;text-shadow:0 1px 0 #fff;top:-.3em}.toast-close-button:focus,.toast-close-button:hover{color:#000;cursor:pointer;-ms-filter:progid:DXImageTransform.Microsoft.Alpha(Opacity=40);filter:alpha(opacity=40);opacity:.4;text-decoration:none}.rtl .toast-close-button{float:left;left:-.3em;right:.3em}button.toast-close-button{-webkit-appearance:none;background:0 0;border:0;cursor:pointer;padding:0}.toast-top-center{right:0;top:0;width:100%}.toast-bottom-center{bottom:0;right:0;width:100%}.toast-top-full-width{right:0;top:0;width:100%}.toast-bottom-full-width{bottom:0;right:0;width:100%}.toast-top-left{left:12px;top:12px}.toast-top-right{right:12px;top:12px}.toast-bottom-right{bottom:12px;right:12px}.toast-bottom-left{bottom:12px;left:12px}#toast-container{pointer-events:none;position:fixed;z-index:999999}#toast-container *{box-sizing:border-box}#toast-container>div{background-position:15px;background-repeat:no-repeat;border-radius:3px;box-shadow:0 0 12px #999;color:#fff;-ms-filter:progid:DXImageTransform.Microsoft.Alpha(Opacity=80);filter:alpha(opacity=80);margin:0 0 6px;opacity:.8;overflow:hidden;padding:15px 15px 15px 50px;pointer-events:auto;position:relative;width:300px}#toast-container>div.rtl{background-position:right 15px center;direction:rtl;padding:15px 50px 15px 15px}#toast-container>div:hover{box-shadow:0 0 12px #000;cursor:pointer;-ms-filter:progid:DXImageTransform.Microsoft.Alpha(Opacity=100);filter:alpha(opacity=100);opacity:1}#toast-container>.toast-info{background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAGwSURBVEhLtZa9SgNBEMc9sUxxRcoUKSzSWIhXpFMhhYWFhaBg4yPYiWCXZxBLERsLRS3EQkEfwCKdjWJAwSKCgoKCcudv4O5YLrt7EzgXhiU3/4+b2ckmwVjJSpKkQ6wAi4gwhT+z3wRBcEz0yjSseUTrcRyfsHsXmD0AmbHOC9Ii8VImnuXBPglHpQ5wwSVM7sNnTG7Za4JwDdCjxyAiH3nyA2mtaTJufiDZ5dCaqlItILh1NHatfN5skvjx9Z38m69CgzuXmZgVrPIGE763Jx9qKsRozWYw6xOHdER+nn2KkO+Bb+UV5CBN6WC6QtBgbRVozrahAbmm6HtUsgtPC19tFdxXZYBOfkbmFJ1VaHA1VAHjd0pp70oTZzvR+EVrx2Ygfdsq6eu55BHYR8hlcki+n+kERUFG8BrA0BwjeAv2M8WLQBtcy+SD6fNsmnB3AlBLrgTtVW1c2QN4bVWLATaIS60J2Du5y1TiJgjSBvFVZgTmwCU+dAZFoPxGEEs8nyHC9Bwe2GvEJv2WXZb0vjdyFT4Cxk3e/kIqlOGoVLwwPevpYHT+00T+hWwXDf4AJAOUqWcDhbwAAAAASUVORK5CYII=)!important}#toast-container>.toast-error{background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAHOSURBVEhLrZa/SgNBEMZzh0WKCClSCKaIYOED+AAKeQQLG8HWztLCImBrYadgIdY+gIKNYkBFSwu7CAoqCgkkoGBI/E28PdbLZmeDLgzZzcx83/zZ2SSXC1j9fr+I1Hq93g2yxH4iwM1vkoBWAdxCmpzTxfkN2RcyZNaHFIkSo10+8kgxkXIURV5HGxTmFuc75B2RfQkpxHG8aAgaAFa0tAHqYFfQ7Iwe2yhODk8+J4C7yAoRTWI3w/4klGRgR4lO7Rpn9+gvMyWp+uxFh8+H+ARlgN1nJuJuQAYvNkEnwGFck18Er4q3egEc/oO+mhLdKgRyhdNFiacC0rlOCbhNVz4H9FnAYgDBvU3QIioZlJFLJtsoHYRDfiZoUyIxqCtRpVlANq0EU4dApjrtgezPFad5S19Wgjkc0hNVnuF4HjVA6C7QrSIbylB+oZe3aHgBsqlNqKYH48jXyJKMuAbiyVJ8KzaB3eRc0pg9VwQ4niFryI68qiOi3AbjwdsfnAtk0bCjTLJKr6mrD9g8iq/S/B81hguOMlQTnVyG40wAcjnmgsCNESDrjme7wfftP4P7SP4N3CJZdvzoNyGq2c/HWOXJGsvVg+RA/k2MC/wN6I2YA2Pt8GkAAAAASUVORK5CYII=)!important}#toast-container>.toast-success{background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAADsSURBVEhLY2AYBfQMgf///3P8+/evAIgvA/FsIF+BavYDDWMBGroaSMMBiE8VC7AZDrIFaMFnii3AZTjUgsUUWUDA8OdAH6iQbQEhw4HyGsPEcKBXBIC4ARhex4G4BsjmweU1soIFaGg/WtoFZRIZdEvIMhxkCCjXIVsATV6gFGACs4Rsw0EGgIIH3QJYJgHSARQZDrWAB+jawzgs+Q2UO49D7jnRSRGoEFRILcdmEMWGI0cm0JJ2QpYA1RDvcmzJEWhABhD/pqrL0S0CWuABKgnRki9lLseS7g2AlqwHWQSKH4oKLrILpRGhEQCw2LiRUIa4lwAAAABJRU5ErkJggg==)!important}#toast-container>.toast-warning{background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAGYSURBVEhL5ZSvTsNQFMbXZGICMYGYmJhAQIJAICYQPAACiSDB8AiICQQJT4CqQEwgJvYASAQCiZiYmJhAIBATCARJy+9rTsldd8sKu1M0+dLb057v6/lbq/2rK0mS/TRNj9cWNAKPYIJII7gIxCcQ51cvqID+GIEX8ASG4B1bK5gIZFeQfoJdEXOfgX4QAQg7kH2A65yQ87lyxb27sggkAzAuFhbbg1K2kgCkB1bVwyIR9m2L7PRPIhDUIXgGtyKw575yz3lTNs6X4JXnjV+LKM/m3MydnTbtOKIjtz6VhCBq4vSm3ncdrD2lk0VgUXSVKjVDJXJzijW1RQdsU7F77He8u68koNZTz8Oz5yGa6J3H3lZ0xYgXBK2QymlWWA+RWnYhskLBv2vmE+hBMCtbA7KX5drWyRT/2JsqZ2IvfB9Y4bWDNMFbJRFmC9E74SoS0CqulwjkC0+5bpcV1CZ8NMej4pjy0U+doDQsGyo1hzVJttIjhQ7GnBtRFN1UarUlH8F3xict+HY07rEzoUGPlWcjRFRr4/gChZgc3ZL2d8oAAAAASUVORK5CYII=)!important}#toast-container.toast-bottom-center>div,#toast-container.toast-top-center>div{margin-left:auto;margin-right:auto;width:300px}#toaLine truncated
|
||||
@@ -208,6 +208,9 @@ window.context = window.context || (function () {
|
||||
let touchGestureId = 0; // 每一次「手指按下」算一个新手势
|
||||
let armedGestureId = 0; // 二级菜单是在哪一次手势里被打开的
|
||||
let submenuArmingTimer = null; // 「刚弹出、先别高亮」的收尾定时器 // 因"点了菜单外"而关菜单后,要吞掉的那一发 click
|
||||
let armedMenuGestureId = 0; // 主菜单是在哪一次手势里被打开的
|
||||
let menuArmingTimer = null; // 主菜单「刚弹出、先别高亮」的收尾定时器
|
||||
let fingerOnScreen = false; // 屏幕上还有没有手指按着(主菜单 arming 窗口据此跟手势续期,而不是死定时器)
|
||||
let menuStylesInjected = false;
|
||||
let menuGuardInstalled = false;
|
||||
|
||||
@@ -241,6 +244,15 @@ window.context = window.context || (function () {
|
||||
+ ' min-width: 0 !important; margin: 0 !important; padding: 0 !important;'
|
||||
+ ' border: 0 !important; border-radius: 0 !important; box-shadow: none !important;'
|
||||
+ ' background: transparent !important; }'
|
||||
// 主菜单版(与下面二级菜单那份同一套 arming 机制,见 armMenuHoverGuard):
|
||||
// 长按弹出的主菜单同样会顶在手指底下,真机触摸的粘滞 hover 会把手指底下那一项当成 hover 目标
|
||||
// → 弹出瞬间那项就跳高亮(老师截图里的「复制图片」)。这里连 li 一起关指针事件:
|
||||
// 只关 a 的话,主题里的 `.dropdown-submenu:hover > a` 仍会因为 li 被 hover 而给「复制链接」上色。
|
||||
// 另外 hover 那套是「底色 + 文字变白」两件一起上(见 context-js.less 的 li>a:hover),
|
||||
// 所以文字色也一并还原 —— 万一 :hover/:focus 还是粘住了,那一项也保持原样,不会变成白底白字。
|
||||
+ '.dropdown-context.menu-arming > li,'
|
||||
+ ' .dropdown-context.menu-arming > li > a {'
|
||||
+ ' pointer-events: none !important; background-color: transparent !important; color: inherit !important; }'
|
||||
// 刚弹出的一瞬间:项不接受指针事件(浏览器就不会把它当成 hover 目标 → 不会高亮),
|
||||
// 同时把 hover 底色压平做双保险;不改变任何尺寸/位置,面板本身照旧接收点击(由守卫吞掉)。
|
||||
+ '.dropdown-context.submenu-inplace.submenu-arming > li.submenu-active > .dropdown-menu > li > a,'
|
||||
@@ -346,6 +358,9 @@ window.context = window.context || (function () {
|
||||
const EDGE_MARGIN = 8;
|
||||
const SUBMENU_GUARD = 350; // 二级菜单「就地替换」后这段毫秒内,触摸点击一律吞掉(防按住误选)
|
||||
const SUBMENU_ARM_MS = 420; // 二级菜单刚弹出的这段毫秒内,先别接受指针指向(防那一下高亮跳色)
|
||||
const MENU_ARM_MS = 420; // 主菜单(长按弹出)同理:弹出后这段时间内的项不接受指针指向
|
||||
const MENU_ARM_TAIL_MS = 260; // 抬手之后再压这么多毫秒 —— 老师原话「手抬起来的那一瞬间留冗余」,
|
||||
// 与二级菜单那份抬手冗余同一个数(见 armMenuHoverGuard / touchend)
|
||||
|
||||
function viewportSize() {
|
||||
const de = document.documentElement;
|
||||
@@ -498,6 +513,50 @@ window.context = window.context || (function () {
|
||||
$('.dropdown-context').removeClass('submenu-arming');
|
||||
}
|
||||
|
||||
// 主菜单版(同一套机制,老师要求「复用到长按弹出的主菜单」):
|
||||
// 主菜单弹在手指底下时,那一项会被浏览器判成 hover 目标 → 弹出瞬间跳高亮,功能和以前一样、就是看着脏。
|
||||
// 做法与二级菜单那份一致:挂 .menu-arming(里面的项不接受指针事件 + hover 底色压平),
|
||||
// 抬手后由 touchend 再续 MENU_ARM_TAIL_MS —— 窗口跟着手势走,不是弹出时定死的一个定时器。
|
||||
// 只在「这一发菜单确实是紧随一次触摸打开」时才挂(判据见 openMenu):桌面右键路径因此一行都不动。
|
||||
function armMenuHoverGuard(ms) {
|
||||
$('.dropdown-context:not(.dropdown-context-sub)').addClass('menu-arming');
|
||||
clearTimeout(menuArmingTimer);
|
||||
menuArmingTimer = setTimeout(function () {
|
||||
// 手指还按着(而且就是按出这个菜单的那次手势)→ 续期:按住多久都不跳色;
|
||||
// 抬手那条路径会再压 MENU_ARM_TAIL_MS 收尾。
|
||||
if (fingerOnScreen && armedMenuGestureId && armedMenuGestureId === touchGestureId) {
|
||||
armMenuHoverGuard(MENU_ARM_MS);
|
||||
return;
|
||||
}
|
||||
$('.dropdown-context').removeClass('menu-arming');
|
||||
}, ms);
|
||||
}
|
||||
|
||||
function clearMenuHoverGuard() {
|
||||
clearTimeout(menuArmingTimer);
|
||||
$('.dropdown-context').removeClass('menu-arming');
|
||||
}
|
||||
|
||||
// arming 生效时面板里的项不接受指针事件(这正是"不跳高亮"的关键),于是手指按在菜单项上时
|
||||
// e.target 拿到的是面板本身 —— 「按一下就展开二级菜单」这条触摸路径会被吃掉。
|
||||
// 这里按触摸坐标把手指底下那一行找回来,只给这一个分支用,保证窗口内的第一次点按与平时一致。
|
||||
function rowUnderTouch(e) {
|
||||
if (! e || ! e.touches || ! e.touches.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const t = e.touches[0], x = t.clientX, y = t.clientY;
|
||||
const rows = document.querySelectorAll('.dropdown-context:not(.dropdown-context-sub) > li');
|
||||
for (let i = 0; i < rows.length; i++) {
|
||||
const r = rows[i].getBoundingClientRect();
|
||||
if (r.width && r.height && x >= r.left && x <= r.right && y >= r.top && y <= r.bottom) {
|
||||
return rows[i];
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
// 打开某个父项的二级菜单:手机惯例是「按下就开」,这样抬手那发合成 click 也有东西可吞。
|
||||
function openSubmenuFor($li) {
|
||||
$li.siblings('.dropdown-submenu').removeClass('touch-open');
|
||||
@@ -685,13 +744,23 @@ window.context = window.context || (function () {
|
||||
// 等抬手才处理就晚了(那时 click 已经落在菜单项上了)。
|
||||
// 抬手:面板若还在「刚弹出」窗口内,把窗口续到抬手之后 —— 按住再久,那一刻也不跳高亮。
|
||||
['touchend', 'touchcancel'].forEach(function (evt) {
|
||||
document.addEventListener(evt, function () {
|
||||
document.addEventListener(evt, function (e) {
|
||||
// 屏幕上还有手指按着吗(多指时抬手一根不算)——主菜单 arming 窗口据此决定要不要续期
|
||||
fingerOnScreen = !! (e && e.touches && e.touches.length);
|
||||
|
||||
// 判据是「这一发手指就是打开二级菜单的那一发」——不能看 .submenu-arming 还在不在:
|
||||
// 按住超过 SUBMENU_ARM_MS 时,那个类早就被定时器摘掉了(踩过)。
|
||||
if (armedGestureId && armedGestureId === touchGestureId
|
||||
&& $('.dropdown-context.submenu-inplace').length) {
|
||||
armSubmenuHoverGuard(260); // 抬手后再压 260ms,抬手那一刻也不会跳色
|
||||
}
|
||||
|
||||
// 主菜单同理(老师原话:「只要给我手抬起来的那一瞬间留冗余就够」)——
|
||||
// 判据同样是「这一发手指就是打开主菜单的那一发」,按住再久也不会在抬手那一刻跳色。
|
||||
if (armedMenuGestureId && armedMenuGestureId === touchGestureId
|
||||
&& $('.dropdown-context:not(.dropdown-context-sub)').length) {
|
||||
armMenuHoverGuard(MENU_ARM_TAIL_MS);
|
||||
}
|
||||
}, true);
|
||||
});
|
||||
|
||||
@@ -700,7 +769,17 @@ window.context = window.context || (function () {
|
||||
|
||||
// 只看「手指底下这个 a 的直接父级」—— 二级菜单的项嵌在父 li 里面,
|
||||
// 用 closest('li.dropdown-submenu') 会把二级菜单里的项也误判成父行。
|
||||
const $a = $(e.target).closest('a');
|
||||
let $a = $(e.target).closest('a');
|
||||
// 主菜单 arming 窗口里,项不接受指针事件 → 手指按在「复制链接」上时 e.target 只是面板,
|
||||
// 这一发就按不到那个 a。按坐标把行找回来,只补「按一下就展开二级菜单」这一条路
|
||||
// (叶子项靠抬手补发的那发 click,本来就不受影响;就地替换态不兜底,那时的行高/内容都变了)。
|
||||
if (! $a.length && $('.dropdown-context.menu-arming').length) {
|
||||
const row = rowUnderTouch(e);
|
||||
if (row && row.classList.contains('dropdown-submenu')
|
||||
&& ! row.classList.contains('submenu-active')) {
|
||||
$a = $(row).children('a');
|
||||
}
|
||||
}
|
||||
const $row = $a.parent();
|
||||
if ($row.hasClass('dropdown-submenu') && $row.closest('.dropdown-context').length) {
|
||||
if ($row.hasClass('submenu-active')) {
|
||||
@@ -710,13 +789,17 @@ window.context = window.context || (function () {
|
||||
}
|
||||
submenuInplaceAt = Date.now();
|
||||
armedGestureId = touchGestureId; // 武装到本次手势:这根手指抬起来的那发 click 必吞
|
||||
} else if ($('.dropdown-context.submenu-arming').length) {
|
||||
clearSubmenuHoverGuard(); // 手指挪去点别的东西了:立刻恢复正常 hover/点击
|
||||
} else {
|
||||
// 手指挪去点别的东西了(包括点菜单项、滑菜单):立刻恢复正常 hover/点击。
|
||||
// 两套 arming 都清 —— 它们各自只对「按出它来的那一次手势」负责,新手势一律失效。
|
||||
clearSubmenuHoverGuard();
|
||||
clearMenuHoverGuard();
|
||||
}
|
||||
}, true);
|
||||
|
||||
document.addEventListener('touchstart', function (e) {
|
||||
lastTouchAt = Date.now();
|
||||
fingerOnScreen = true; // 手指按下:主菜单 arming 窗口可以跟手势续期
|
||||
|
||||
if (! menuOnScreen() || isInsideMenu(e.target)) {
|
||||
return;
|
||||
@@ -937,6 +1020,15 @@ window.context = window.context || (function () {
|
||||
menuOpenedAt = Date.now();
|
||||
menuClosingUntil = 0;
|
||||
|
||||
// 刚弹出的主菜单会顶在手指底下:那一项马上会被当成 hover 目标 → 弹出瞬间就跳高亮。
|
||||
// 复用二级菜单那套 arming:挂 .menu-arming(项不接受指针事件 + hover 底色压平),抬手后再续 260ms。
|
||||
// **只在「这一发菜单确实紧随一次触摸」时才挂**:鼠标右键路径(lastTouchAt 是 0 或很久以前)
|
||||
// 与上游完全一致 —— 桌面本来没有粘滞 hover,不该为此改动付出任何代价。
|
||||
if ((Date.now() - lastTouchAt) < MENU_OPEN_GRACE) {
|
||||
armedMenuGestureId = touchGestureId;
|
||||
armMenuHoverGuard(MENU_ARM_MS);
|
||||
}
|
||||
|
||||
logMenuEvent('open trigger=' + (evt ? 'contextmenu' : 'longpress')
|
||||
+ ' item=' + describeNode(item)
|
||||
+ ' at=' + Math.round(pageX) + ',' + Math.round(pageY));
|
||||
|
||||
@@ -391,6 +391,112 @@ html {
|
||||
.swal2-close:hover {
|
||||
color: var(--lsky-text);
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// 弹出/关闭动画:与 x-modal 的 x-transition 对齐
|
||||
// x-modal:enter = transition ease-out duration-300(桌面 opacity 0→1 + scale .95→1;
|
||||
// 手机 opacity 0→1 + translate-y-4→0)
|
||||
// leave = transition ease-in duration-200(同起止反向)
|
||||
// sweetalert2 自带的 .swal2-show 是 300ms 线性 scale(.7→1.05→.95→1)、.swal2-hide 是
|
||||
// 150ms 线性 scale(1→.5),都没有淡入淡出 → 观感与新弹窗不是一套。
|
||||
// 这里直接改这两个类的 animation(sweetalert2 的可见性判断只看「有没有动画 + 动画结束事件」,
|
||||
// 时长/关键帧换成我们的照样会等动画结束再移除节点,关闭动画因此也一致)。
|
||||
// ease-out = cubic-bezier(0,0,.2,1),ease-in = cubic-bezier(.4,0,1,1)(就是 Tailwind 的两个缓动)。
|
||||
// -----------------------------------------------------------------------
|
||||
.swal2-show {
|
||||
animation: lsky-swal-in .3s cubic-bezier(0, 0, .2, 1) both;
|
||||
}
|
||||
|
||||
.swal2-hide {
|
||||
animation: lsky-swal-out .2s cubic-bezier(.4, 0, 1, 1) both;
|
||||
}
|
||||
|
||||
@media (max-width: 767px) {
|
||||
// md 以下(<768px)= x-modal 的「不上缩、整块往上滑 16px」那一档
|
||||
.swal2-show {
|
||||
animation-name: lsky-swal-in-mobile;
|
||||
}
|
||||
|
||||
.swal2-hide {
|
||||
animation-name: lsky-swal-out-mobile;
|
||||
}
|
||||
}
|
||||
|
||||
// 遮罩跟着弹窗一起淡:容器原本是 `transition: background-color .1s`(关闭时闪一下、跟弹窗不是一套)。
|
||||
// 注意只能写 background-color 这一个属性:如果把 backdrop-filter 也列进 transition,
|
||||
// Chromium 下 background-color 的过渡会整条不生效(实测遮罩 0.5 → 0 是硬切)。
|
||||
.swal2-container {
|
||||
transition: background-color .2s cubic-bezier(.4, 0, 1, 1);
|
||||
}
|
||||
|
||||
.swal2-container.swal2-backdrop-show,
|
||||
.swal2-container.swal2-noanimation {
|
||||
transition-duration: .3s;
|
||||
transition-timing-function: cubic-bezier(0, 0, .2, 1);
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// 斩断 sweetalert2 对 <body> 的动手脚(这是「弹出后背景元素会挪一下」的真因)
|
||||
// -----------------------------------------------------------------------
|
||||
// 它打开时会:① 给 html/body 加 swal2-shown(默认样式里 body.swal2-shown 是 overflow:hidden,
|
||||
// 滚动条被拿走 → 内容宽度变了 → 整页重排);② 由 JS 往 body.style 写 padding-right 来「抵消」
|
||||
// 滚动条宽度(行内样式,CSS 盖不住,只能在调用处用 scrollbarPadding:false 关掉,见 images.blade.php)。
|
||||
// 这里负责 ①:权重 (0,3,2) > 它的 (0,3,1),不用 !important。用可见值(body 打开前的原值就是
|
||||
// visible)而不是 auto —— auto 会让 body 自己变成滚动容器,弹窗开着的那几秒里 sticky 定位的
|
||||
// 参照滚动盒会变。背景能像 x-modal 那样照常滚动,也就不会重排 —— 手机上新弹窗本来也是不锁背景
|
||||
// 滚动的,两端行为一致。
|
||||
body.swal2-shown:not(.swal2-no-backdrop):not(.swal2-toast-shown) {
|
||||
overflow: visible;
|
||||
}
|
||||
}
|
||||
|
||||
/* sweetalert2 弹出/关闭动画的关键帧(= x-modal 的 enter/leave 起止值) */
|
||||
@keyframes lsky-swal-in {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: scale(.95);
|
||||
}
|
||||
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes lsky-swal-out {
|
||||
from {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
}
|
||||
|
||||
to {
|
||||
opacity: 0;
|
||||
transform: scale(.95);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes lsky-swal-in-mobile {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(16px);
|
||||
}
|
||||
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes lsky-swal-out-mobile {
|
||||
from {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
to {
|
||||
opacity: 0;
|
||||
transform: translateY(16px);
|
||||
}
|
||||
}
|
||||
|
||||
/* ---------------------------------------------------------------------------
|
||||
|
||||
@@ -208,6 +208,9 @@ window.context = window.context || (function () {
|
||||
let touchGestureId = 0; // 每一次「手指按下」算一个新手势
|
||||
let armedGestureId = 0; // 二级菜单是在哪一次手势里被打开的
|
||||
let submenuArmingTimer = null; // 「刚弹出、先别高亮」的收尾定时器 // 因"点了菜单外"而关菜单后,要吞掉的那一发 click
|
||||
let armedMenuGestureId = 0; // 主菜单是在哪一次手势里被打开的
|
||||
let menuArmingTimer = null; // 主菜单「刚弹出、先别高亮」的收尾定时器
|
||||
let fingerOnScreen = false; // 屏幕上还有没有手指按着(主菜单 arming 窗口据此跟手势续期,而不是死定时器)
|
||||
let menuStylesInjected = false;
|
||||
let menuGuardInstalled = false;
|
||||
|
||||
@@ -241,6 +244,15 @@ window.context = window.context || (function () {
|
||||
+ ' min-width: 0 !important; margin: 0 !important; padding: 0 !important;'
|
||||
+ ' border: 0 !important; border-radius: 0 !important; box-shadow: none !important;'
|
||||
+ ' background: transparent !important; }'
|
||||
// 主菜单版(与下面二级菜单那份同一套 arming 机制,见 armMenuHoverGuard):
|
||||
// 长按弹出的主菜单同样会顶在手指底下,真机触摸的粘滞 hover 会把手指底下那一项当成 hover 目标
|
||||
// → 弹出瞬间那项就跳高亮(老师截图里的「复制图片」)。这里连 li 一起关指针事件:
|
||||
// 只关 a 的话,主题里的 `.dropdown-submenu:hover > a` 仍会因为 li 被 hover 而给「复制链接」上色。
|
||||
// 另外 hover 那套是「底色 + 文字变白」两件一起上(见 context-js.less 的 li>a:hover),
|
||||
// 所以文字色也一并还原 —— 万一 :hover/:focus 还是粘住了,那一项也保持原样,不会变成白底白字。
|
||||
+ '.dropdown-context.menu-arming > li,'
|
||||
+ ' .dropdown-context.menu-arming > li > a {'
|
||||
+ ' pointer-events: none !important; background-color: transparent !important; color: inherit !important; }'
|
||||
// 刚弹出的一瞬间:项不接受指针事件(浏览器就不会把它当成 hover 目标 → 不会高亮),
|
||||
// 同时把 hover 底色压平做双保险;不改变任何尺寸/位置,面板本身照旧接收点击(由守卫吞掉)。
|
||||
+ '.dropdown-context.submenu-inplace.submenu-arming > li.submenu-active > .dropdown-menu > li > a,'
|
||||
@@ -346,6 +358,9 @@ window.context = window.context || (function () {
|
||||
const EDGE_MARGIN = 8;
|
||||
const SUBMENU_GUARD = 350; // 二级菜单「就地替换」后这段毫秒内,触摸点击一律吞掉(防按住误选)
|
||||
const SUBMENU_ARM_MS = 420; // 二级菜单刚弹出的这段毫秒内,先别接受指针指向(防那一下高亮跳色)
|
||||
const MENU_ARM_MS = 420; // 主菜单(长按弹出)同理:弹出后这段时间内的项不接受指针指向
|
||||
const MENU_ARM_TAIL_MS = 260; // 抬手之后再压这么多毫秒 —— 老师原话「手抬起来的那一瞬间留冗余」,
|
||||
// 与二级菜单那份抬手冗余同一个数(见 armMenuHoverGuard / touchend)
|
||||
|
||||
function viewportSize() {
|
||||
const de = document.documentElement;
|
||||
@@ -498,6 +513,50 @@ window.context = window.context || (function () {
|
||||
$('.dropdown-context').removeClass('submenu-arming');
|
||||
}
|
||||
|
||||
// 主菜单版(同一套机制,老师要求「复用到长按弹出的主菜单」):
|
||||
// 主菜单弹在手指底下时,那一项会被浏览器判成 hover 目标 → 弹出瞬间跳高亮,功能和以前一样、就是看着脏。
|
||||
// 做法与二级菜单那份一致:挂 .menu-arming(里面的项不接受指针事件 + hover 底色压平),
|
||||
// 抬手后由 touchend 再续 MENU_ARM_TAIL_MS —— 窗口跟着手势走,不是弹出时定死的一个定时器。
|
||||
// 只在「这一发菜单确实是紧随一次触摸打开」时才挂(判据见 openMenu):桌面右键路径因此一行都不动。
|
||||
function armMenuHoverGuard(ms) {
|
||||
$('.dropdown-context:not(.dropdown-context-sub)').addClass('menu-arming');
|
||||
clearTimeout(menuArmingTimer);
|
||||
menuArmingTimer = setTimeout(function () {
|
||||
// 手指还按着(而且就是按出这个菜单的那次手势)→ 续期:按住多久都不跳色;
|
||||
// 抬手那条路径会再压 MENU_ARM_TAIL_MS 收尾。
|
||||
if (fingerOnScreen && armedMenuGestureId && armedMenuGestureId === touchGestureId) {
|
||||
armMenuHoverGuard(MENU_ARM_MS);
|
||||
return;
|
||||
}
|
||||
$('.dropdown-context').removeClass('menu-arming');
|
||||
}, ms);
|
||||
}
|
||||
|
||||
function clearMenuHoverGuard() {
|
||||
clearTimeout(menuArmingTimer);
|
||||
$('.dropdown-context').removeClass('menu-arming');
|
||||
}
|
||||
|
||||
// arming 生效时面板里的项不接受指针事件(这正是"不跳高亮"的关键),于是手指按在菜单项上时
|
||||
// e.target 拿到的是面板本身 —— 「按一下就展开二级菜单」这条触摸路径会被吃掉。
|
||||
// 这里按触摸坐标把手指底下那一行找回来,只给这一个分支用,保证窗口内的第一次点按与平时一致。
|
||||
function rowUnderTouch(e) {
|
||||
if (! e || ! e.touches || ! e.touches.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const t = e.touches[0], x = t.clientX, y = t.clientY;
|
||||
const rows = document.querySelectorAll('.dropdown-context:not(.dropdown-context-sub) > li');
|
||||
for (let i = 0; i < rows.length; i++) {
|
||||
const r = rows[i].getBoundingClientRect();
|
||||
if (r.width && r.height && x >= r.left && x <= r.right && y >= r.top && y <= r.bottom) {
|
||||
return rows[i];
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
// 打开某个父项的二级菜单:手机惯例是「按下就开」,这样抬手那发合成 click 也有东西可吞。
|
||||
function openSubmenuFor($li) {
|
||||
$li.siblings('.dropdown-submenu').removeClass('touch-open');
|
||||
@@ -685,13 +744,23 @@ window.context = window.context || (function () {
|
||||
// 等抬手才处理就晚了(那时 click 已经落在菜单项上了)。
|
||||
// 抬手:面板若还在「刚弹出」窗口内,把窗口续到抬手之后 —— 按住再久,那一刻也不跳高亮。
|
||||
['touchend', 'touchcancel'].forEach(function (evt) {
|
||||
document.addEventListener(evt, function () {
|
||||
document.addEventListener(evt, function (e) {
|
||||
// 屏幕上还有手指按着吗(多指时抬手一根不算)——主菜单 arming 窗口据此决定要不要续期
|
||||
fingerOnScreen = !! (e && e.touches && e.touches.length);
|
||||
|
||||
// 判据是「这一发手指就是打开二级菜单的那一发」——不能看 .submenu-arming 还在不在:
|
||||
// 按住超过 SUBMENU_ARM_MS 时,那个类早就被定时器摘掉了(踩过)。
|
||||
if (armedGestureId && armedGestureId === touchGestureId
|
||||
&& $('.dropdown-context.submenu-inplace').length) {
|
||||
armSubmenuHoverGuard(260); // 抬手后再压 260ms,抬手那一刻也不会跳色
|
||||
}
|
||||
|
||||
// 主菜单同理(老师原话:「只要给我手抬起来的那一瞬间留冗余就够」)——
|
||||
// 判据同样是「这一发手指就是打开主菜单的那一发」,按住再久也不会在抬手那一刻跳色。
|
||||
if (armedMenuGestureId && armedMenuGestureId === touchGestureId
|
||||
&& $('.dropdown-context:not(.dropdown-context-sub)').length) {
|
||||
armMenuHoverGuard(MENU_ARM_TAIL_MS);
|
||||
}
|
||||
}, true);
|
||||
});
|
||||
|
||||
@@ -700,7 +769,17 @@ window.context = window.context || (function () {
|
||||
|
||||
// 只看「手指底下这个 a 的直接父级」—— 二级菜单的项嵌在父 li 里面,
|
||||
// 用 closest('li.dropdown-submenu') 会把二级菜单里的项也误判成父行。
|
||||
const $a = $(e.target).closest('a');
|
||||
let $a = $(e.target).closest('a');
|
||||
// 主菜单 arming 窗口里,项不接受指针事件 → 手指按在「复制链接」上时 e.target 只是面板,
|
||||
// 这一发就按不到那个 a。按坐标把行找回来,只补「按一下就展开二级菜单」这一条路
|
||||
// (叶子项靠抬手补发的那发 click,本来就不受影响;就地替换态不兜底,那时的行高/内容都变了)。
|
||||
if (! $a.length && $('.dropdown-context.menu-arming').length) {
|
||||
const row = rowUnderTouch(e);
|
||||
if (row && row.classList.contains('dropdown-submenu')
|
||||
&& ! row.classList.contains('submenu-active')) {
|
||||
$a = $(row).children('a');
|
||||
}
|
||||
}
|
||||
const $row = $a.parent();
|
||||
if ($row.hasClass('dropdown-submenu') && $row.closest('.dropdown-context').length) {
|
||||
if ($row.hasClass('submenu-active')) {
|
||||
@@ -710,13 +789,17 @@ window.context = window.context || (function () {
|
||||
}
|
||||
submenuInplaceAt = Date.now();
|
||||
armedGestureId = touchGestureId; // 武装到本次手势:这根手指抬起来的那发 click 必吞
|
||||
} else if ($('.dropdown-context.submenu-arming').length) {
|
||||
clearSubmenuHoverGuard(); // 手指挪去点别的东西了:立刻恢复正常 hover/点击
|
||||
} else {
|
||||
// 手指挪去点别的东西了(包括点菜单项、滑菜单):立刻恢复正常 hover/点击。
|
||||
// 两套 arming 都清 —— 它们各自只对「按出它来的那一次手势」负责,新手势一律失效。
|
||||
clearSubmenuHoverGuard();
|
||||
clearMenuHoverGuard();
|
||||
}
|
||||
}, true);
|
||||
|
||||
document.addEventListener('touchstart', function (e) {
|
||||
lastTouchAt = Date.now();
|
||||
fingerOnScreen = true; // 手指按下:主菜单 arming 窗口可以跟手势续期
|
||||
|
||||
if (! menuOnScreen() || isInsideMenu(e.target)) {
|
||||
return;
|
||||
@@ -937,6 +1020,15 @@ window.context = window.context || (function () {
|
||||
menuOpenedAt = Date.now();
|
||||
menuClosingUntil = 0;
|
||||
|
||||
// 刚弹出的主菜单会顶在手指底下:那一项马上会被当成 hover 目标 → 弹出瞬间就跳高亮。
|
||||
// 复用二级菜单那套 arming:挂 .menu-arming(项不接受指针事件 + hover 底色压平),抬手后再续 260ms。
|
||||
// **只在「这一发菜单确实紧随一次触摸」时才挂**:鼠标右键路径(lastTouchAt 是 0 或很久以前)
|
||||
// 与上游完全一致 —— 桌面本来没有粘滞 hover,不该为此改动付出任何代价。
|
||||
if ((Date.now() - lastTouchAt) < MENU_OPEN_GRACE) {
|
||||
armedMenuGestureId = touchGestureId;
|
||||
armMenuHoverGuard(MENU_ARM_MS);
|
||||
}
|
||||
|
||||
logMenuEvent('open trigger=' + (evt ? 'contextmenu' : 'longpress')
|
||||
+ ' item=' + describeNode(item)
|
||||
+ ' at=' + Math.round(pageX) + ',' + Math.round(pageY));
|
||||
|
||||
@@ -1,12 +1,17 @@
|
||||
@props(['id' => 'modal'])
|
||||
|
||||
{{-- 高度一律用动态视口单位 dvh:手机地址栏可见时 100vh 比"看得见的区域"高,
|
||||
弹窗会伸出屏幕外(下滑就能看到底下一块空白)。100dvh 跟着地址栏收起/展开实时变,
|
||||
两种情形都刚好铺满。--}}
|
||||
<div {{ $attributes->merge(['id' => $id, 'class' => "fixed z-10 inset-0 h-[100dvh] overflow-y-auto"]) }} role="dialog" aria-modal="true" x-data x-cloak x-show="$store.modal.isOpen('{{ $id }}')">
|
||||
{{-- min-h-screen(100vh) 保留在前面当兜底:浏览器不认识 dvh 时整条声明作废,退回 100vh(旧行为)。
|
||||
桌面上 100dvh === 100vh,所以这一行对桌面零影响。 --}}
|
||||
<div class="flex min-h-screen min-h-[100dvh] text-center md:block md:px-2 lg:px-4" style="font-size: 0">
|
||||
{{-- 高度一律用动态视口单位 dvh:手机地址栏可见时 100vh 比"看得见的区域"高,越界那一截就是底部空带。
|
||||
100dvh 跟着地址栏收起/展开实时变,两种情形都刚好铺满。--}}
|
||||
{{-- overscroll-contain(= overscroll-behavior:contain)加在弹窗自己的滚动容器上:弹窗里滚到尽头后
|
||||
不再把滚动"接力"给背后的页面。安卓上滚动一旦传给页面,地址栏会跟着收起、可视视口变高,
|
||||
弹窗底部就空出一块——这条"接力"才是那块空带的入口。
|
||||
触发链:弹窗内滚到底 → 滚动接力到页面 → 地址栏收起/视口变高 → 弹窗底部露出空带(已实测复现)。--}}
|
||||
<div {{ $attributes->merge(['id' => $id, 'class' => "fixed z-10 inset-0 h-[100dvh] overflow-y-auto overscroll-contain"]) }} role="dialog" aria-modal="true" x-data x-cloak x-show="$store.modal.isOpen('{{ $id }}')">
|
||||
{{-- 内层 min-height 用「vh 声明在前、dvh 声明在后」的内联写法:同一对大括号里后者胜,
|
||||
与 Tailwind 的生成顺序无关(两个工具类同名属性谁赢取决于样式表顺序,不稳)。
|
||||
支持 dvh 的浏览器用 dvh(= 真实可见视口);不认识的旧浏览器整条 dvh 声明作废,
|
||||
自动退回紧邻的 100vh(= 旧行为)。桌面上 100dvh === 100vh,函数等价。 --}}
|
||||
<div class="flex text-center md:block md:px-2 lg:px-4" style="font-size: 0; min-height: 100vh; min-height: 100dvh">
|
||||
<div x-transition:enter="transition ease-out duration-300"
|
||||
x-transition:enter-start="transform opacity-0"
|
||||
x-transition:enter-end="transform opacity-100"
|
||||
@@ -19,7 +24,8 @@ class="hidden fixed inset-0 bg-black/50 backdrop-blur-[2px] transition-opacity m
|
||||
aria-hidden="true"
|
||||
>
|
||||
</div>
|
||||
<span class="hidden md:inline-block md:align-middle md:h-screen" aria-hidden="true">​</span>
|
||||
{{-- 桌面端垂直居中的撑高符同样走「vh 在前、dvh 在后」,链路上不再留 md:h-screen(100vh)。 --}}
|
||||
<span class="hidden md:inline-block md:align-middle" style="height: 100vh; height: 100dvh" aria-hidden="true">​</span>
|
||||
|
||||
<div x-transition:enter="transition ease-out duration-300"
|
||||
x-transition:enter-start="transform opacity-0 translate-y-4 md:translate-y-0 md:scale-95"
|
||||
|
||||
@@ -3,7 +3,22 @@
|
||||
<x-slot name="trigger">
|
||||
<button type="button" class="flex items-center justify-center gap-2 h-10 w-10 p-0 rounded-full border border-line hover:bg-surface-2 text-[14px] text-ink sm:w-auto sm:pl-1 sm:pr-2" id="user-menu-button" aria-expanded="false" aria-haspopup="true">
|
||||
<span class="sr-only">Open user menu</span>
|
||||
<img class="h-7 w-7 rounded-full object-cover" src="{{ Auth::user()->avatar }}" alt="">
|
||||
{{-- 头像占位框:固定 28×28(h-7 w-7 + width/height 属性),图片没到/挂了都不抖动。
|
||||
里面两层:底下的「名字首字」色块圆圈(兜底)与盖在它上面的 img。
|
||||
真地址放在 data-avatar-src —— 由文件末尾的内联脚本在页面 load 之后再接上,
|
||||
这样外站头像(cravatar)再慢也不会拖住整页的 load(之前的「页面一直没加载完」)。
|
||||
img 加载失败/被阻 → onerror 把自己撤掉,露出首字圆圈;加载成功 → 去掉首字(防透明图透字)。 --}}
|
||||
{{-- overflow-hidden 是必需的:flex 项的 min-width:auto 会按内容最小宽度撑开,
|
||||
名字首字要是被换成长字符串(或字体异常),整个按钮会被顶宽 —— 加了它之后
|
||||
内容再大也只在这个 28×28 的圆圈里裁掉,按钮尺寸不受影响。 --}}
|
||||
<span class="relative flex h-7 w-7 shrink-0 items-center justify-center overflow-hidden rounded-full bg-surface-2 text-[11px] font-medium text-ink-2" id="user-avatar">
|
||||
<span aria-hidden="true" data-avatar-fallback>{{ mb_substr(trim(Auth::user()->name), 0, 1) ?: '?' }}</span>
|
||||
<img class="absolute inset-0 h-7 w-7 rounded-full object-cover opacity-0 transition-opacity duration-200"
|
||||
id="user-avatar-img" alt="" width="28" height="28" decoding="async"
|
||||
data-avatar-src="{{ Auth::user()->avatar }}"
|
||||
onload="this.previousElementSibling && this.previousElementSibling.remove(); this.classList.remove('opacity-0');"
|
||||
onerror="this.remove();">
|
||||
</span>
|
||||
<span class="px-1 sm:block hidden text-ink-2">{{ Auth::user()->name }}</span>
|
||||
</button>
|
||||
</x-slot>
|
||||
@@ -21,3 +36,24 @@
|
||||
</form>
|
||||
</x-slot>
|
||||
</x-dropdown>
|
||||
|
||||
{{-- 头像的延迟加载:等页面 load 完(其它资源都好了)再把 data-avatar-src 接到 img.src 上。
|
||||
不引入任何依赖;header / welcome 都会 include 本文件,重复 include 也安全(一个 flag + querySelectorAll 幂等)。 --}}
|
||||
<script>
|
||||
(function () {
|
||||
if (window.__userNavAvatarInit) {
|
||||
return;
|
||||
}
|
||||
window.__userNavAvatarInit = true;
|
||||
var fill = function () {
|
||||
document.querySelectorAll('img[data-avatar-src]').forEach(function (img) {
|
||||
img.src = img.getAttribute('data-avatar-src');
|
||||
});
|
||||
};
|
||||
if (document.readyState === 'complete') {
|
||||
fill();
|
||||
} else {
|
||||
window.addEventListener('load', fill);
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
@@ -469,6 +469,12 @@ classes: ['dragselect'],
|
||||
$albums.off('click', '.delete').on('click', '.delete', function (e) {
|
||||
e.stopPropagation();
|
||||
Swal.fire({
|
||||
// 与新弹窗(x-modal)一套:不动背景页面。
|
||||
// heightAuto 关掉 html/body 上的 swal2-height-auto;scrollbarPadding 关掉
|
||||
// sweetalert2 往 body 写 padding-right 的那一步(它按「滚动条宽度」补内边距,
|
||||
// 手机上明明没有占位滚动条,补 8px 却真的会把内容挤窄 → 工具栏换行、整页下移)。
|
||||
heightAuto: false,
|
||||
scrollbarPadding: false,
|
||||
title: '确认删除该相册?',
|
||||
text: "删除后相册中的图片将会被移出。",
|
||||
icon: 'warning',
|
||||
@@ -746,6 +752,10 @@ classes: ['dragselect'],
|
||||
},
|
||||
permission() {
|
||||
Swal.fire({
|
||||
// 与新弹窗(x-modal)一套:不动背景页面(heightAuto 去 swal2-height-auto,
|
||||
// scrollbarPadding 去 body 的 padding-right 注入 —— 否则整页会有细微位移)
|
||||
heightAuto: false,
|
||||
scrollbarPadding: false,
|
||||
title: '选择一个权限',
|
||||
text: '选择公开后所有用户都能看到这张图片',
|
||||
input: 'select',
|
||||
@@ -785,6 +795,10 @@ classes: ['dragselect'],
|
||||
rename(e) {
|
||||
let item = $(e).data('json');
|
||||
Swal.fire({
|
||||
// 与新弹窗(x-modal)一套:不动背景页面(heightAuto 去 swal2-height-auto,
|
||||
// scrollbarPadding 去 body 的 padding-right 注入 —— 否则整页会有细微位移)
|
||||
heightAuto: false,
|
||||
scrollbarPadding: false,
|
||||
title: '请输入图片名称',
|
||||
input: 'text',
|
||||
inputValue: item.filename,
|
||||
@@ -821,6 +835,10 @@ classes: ['dragselect'],
|
||||
},
|
||||
delete() {
|
||||
Swal.fire({
|
||||
// 与新弹窗(x-modal)一套:不动背景页面(heightAuto 去 swal2-height-auto,
|
||||
// scrollbarPadding 去 body 的 padding-right 注入 —— 否则整页会有细微位移)
|
||||
heightAuto: false,
|
||||
scrollbarPadding: false,
|
||||
title: '确认要删除选中的图片?',
|
||||
text: "删除后不可恢复,记录和文件同时删除",
|
||||
icon: 'warning',
|
||||
|
||||
Reference in new issue
Block a user