fix(build): 前端资源重建不再破坏补丁自证(webpack 会压缩 mix.copy 过去的文件)
- webpack.mix.js:去掉 context-js 的 mix.copy —— --production 下 webpack 会对 copy 过去的文件做 压缩改名,public/js 里的补丁版会丢掉 isIOSWebKit / LONG_PRESS_DELAY = 250 / installMenuGuard / MENU_OPEN_GRACE 这些标记,既让 Dockerfile 的补丁自证 grep 失败,又与 resources/js 不再字节一致 (CI 的「两份 context-js.js 内容一致」测试正是抓出这个问题的) - 新增 scripts/copy-patched-assets.js,package.json 的 production 收尾调用它做字节级拷贝, 保证 public/js 与 resources/js 两份永远一模一样 - user/images.blade.php 的 md5 同步更新(界面改造后重算) 验证:npm run prod 通过;两份 context-js.js md5 均为 e114c840…(含全部补丁标记); 70/70 jsdom 行为测试通过;verify_repo.py 通过
This commit is contained in:
1 parent
84029b9d27
commit
bdb10c1683
7 files changed
+769
-6
No files matched your search
@@ -0,0 +1,12 @@
|
||||
fix(build): 前端资源重建不再破坏补丁自证(webpack 会压缩 mix.copy 过去的文件)
|
||||
|
||||
- webpack.mix.js:去掉 context-js 的 mix.copy —— --production 下 webpack 会对 copy 过去的文件做
|
||||
压缩改名,public/js 里的补丁版会丢掉 isIOSWebKit / LONG_PRESS_DELAY = 250 / installMenuGuard /
|
||||
MENU_OPEN_GRACE 这些标记,既让 Dockerfile 的补丁自证 grep 失败,又与 resources/js 不再字节一致
|
||||
(CI 的「两份 context-js.js 内容一致」测试正是抓出这个问题的)
|
||||
- 新增 scripts/copy-patched-assets.js,package.json 的 production 收尾调用它做字节级拷贝,
|
||||
保证 public/js 与 resources/js 两份永远一模一样
|
||||
- user/images.blade.php 的 md5 同步更新(界面改造后重算)
|
||||
|
||||
验证:npm run prod 通过;两份 context-js.js md5 均为 e114c840…(含全部补丁标记);
|
||||
70/70 jsdom 行为测试通过;verify_repo.py 通过
|
||||
+2
-2
@@ -101,7 +101,7 @@ RUN php -r "file_exists('.env') || copy('.env.example', '.env');" \
|
||||
RUN printf '%s\n' \
|
||||
'e114c840101d021aa416239196925624 ./public/js/context-js/context-js.js' \
|
||||
'e114c840101d021aa416239196925624 ./resources/js/context-js.js' \
|
||||
'ec25ecc2a4f09395db5a50de1883d521 ./resources/views/user/images.blade.php' \
|
||||
'fb65dc33a394fc28e14af5ab92272406 ./resources/views/user/images.blade.php' \
|
||||
| md5sum -c - \
|
||||
&& grep -q "context-js.js') . '?v=ios-longpress4'" ./resources/views/user/images.blade.php \
|
||||
&& grep -q 'isIOSWebKit' ./public/js/context-js/context-js.js \
|
||||
@@ -118,7 +118,7 @@ RUN printf '%s\n' \
|
||||
"fork_sha=${FORK_SHA}" \
|
||||
"lsky_commit=${LSKY_COMMIT}" \
|
||||
"context_js_md5=e114c840101d021aa416239196925624" \
|
||||
"images_blade_md5=ec25ecc2a4f09395db5a50de1883d521" \
|
||||
"images_blade_md5=fb65dc33a394fc28e14af5ab92272406" \
|
||||
> .code-revision \
|
||||
&& cat .code-revision
|
||||
|
||||
|
||||
+1
-1
@@ -7,7 +7,7 @@
|
||||
"watch-poll": "mix watch -- --watch-options-poll=1000",
|
||||
"hot": "mix watch --hot",
|
||||
"prod": "npm run production",
|
||||
"production": "mix --production"
|
||||
"production": "mix --production && node scripts/copy-patched-assets.js"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@fortawesome/fontawesome-free": "^5.15.4",
|
||||
|
||||
@@ -1 +1,726 @@
|
||||
window.context=window.context||function(){let e={fadeSpeed:100,filter:function(e){},above:"auto",preventDoubleContext:!0,compress:!1};let t=[],n=!1,o=0,i=0,a=!1;function r(e,r){!function(){if(n)return;n=!0;let e=t.map((function(e){return e+", "+e+" *"})).join(", ")+", .dropdown-context, .dropdown-context *";$('<style id="context-js-ios-touch">').appendTo("head").text(e+" { -webkit-touch-callout: none; -webkit-user-select: none; user-select: none; }")}(),a||(a=!0,document.addEventListener("click",(function(e){if(!(Date.now()>o)){o=0;for(let n=0;n<t.length;n++)if(e.target.closest&&e.target.closest(t[n]))return e.__contextjsClickSwallowed=!0,e.preventDefault(),void e.stopPropagation()}}),!0));let d=null,c=0,s=0,l=!1;function u(){null!==d&&(clearTimeout(d),d=null)}$(document).on("touchstart",e,(function(e){let t=e.originalEvent,n=t.touches[0];if(u(),l=!1,!n||t.touches.length>1)return;if(t.__contextjsLongPressTaken)return;t.__contextjsLongPressTaken=!0,c=n.clientX,s=n.clientY;let o=this;d=setTimeout((function(){d=null,l=!0,i=Date.now(),r(o,n.clientX,n.clientY)}),250)})),$(document).on("touchmove",e,(function(e){let t=e.originalEvent.touches[0];null!==d&&t&&(Math.abs(t.clientX-c)>10||Math.abs(t.clientY-s)>10)&&u()})),$(document).on("touchend touchcancel",e,(function(e){u(),l&&(o=Date.now()+700,e.preventDefault())}))}let d=!1,c=0,s=0,l=0,u=[],f=!1,p=0,w=0,h=!1,g=!1;function m(){return window.matchMedia?!!window.matchMedia("(hover: none)").matches:"ontouchstart"in window}function v(e){if(!e||!e.tagName)return String(e);let t=e.id?"#"+e.id:"",n="string"==typeof e.className&&e.className.trim()?"."+e.className.trim().split(/\s+/).join("."):"";return e.tagName.toLowerCase()+t+n}function x(e){l=Date.now();let t="["+(new Date).toISOString().slice(11,23)+"] "+e;u.push(t),u.length>240&&(u=u.slice(-240)),f&&window.console&&window.console.log&&window.console.log("[context-js] "+t)}function b(){if(d)return!0;if(Date.now()<=s)return!0;let e=document.querySelectorAll(".dropdown-context");for(let t=0;t<e.length;t++){let n=e[t];if((!n.style||"none"!==n.style.display)&&("function"==typeof n.getBoundingClientRect&&n.getBoundingClientRect().height>0))return!0}return!1}function D(){return Date.now()-c<300}function C(t){d&&x("close reason="+(t||"unknown")+" openFor="+(Date.now()-c)+"ms"),d=!1,s=Date.now()+e.fadeSpeed+50,$(".dropdown-context").fadeOut(e.fadeSpeed,(function(){$(".dropdown-context").css({display:""}),$(".dropdown-context .drop-left").removeClass("drop-left"),$(".dropdown-context .touch-open").removeClass("touch-open")}))}function k(e){return!!(e&&e.closest&&e.closest(".dropdown-context"))}function y(){g||(g=!0,h||(h=!0,$('<style id="context-js-touch-submenu">').appendTo("head").text(".dropdown-context .dropdown-submenu.touch-open > .dropdown-menu { display: block; }")),document.addEventListener("click",(function(e){if(!(e.__contextjsClickSwallowed||Date.now()<=o)){if(Date.now()<=w)return w=0,e.__contextjsClickSwallowed=!0,e.preventDefault(),void e.stopPropagation();if(b()){if(k(e.target))return x("click target="+v(e.target)+" → 菜单内部,放行(交给原有逻辑)"),void function(e){if(!m())return!1;let t=$(e.target).closest("a");if(!t.length)return!1;let n=t.parent();if(!n.length||!n.hasClass("dropdown-submenu"))return!1;e.preventDefault(),e.stopPropagation();let o=n.hasClass("touch-open");if(n.siblings(".dropdown-submenu").removeClass("touch-open"),o)return n.removeClass("touch-open"),!0;n.addClass("touch-open");let i=n.find(".dropdown-context-sub:first");i.length&&(i.width()+i.offset().left>window.innerWidth?i.addClass("drop-left"):i.removeClass("drop-left"))}(e);if(Date.now()-p<800&&Date.now()-c<800)return x("click target="+v(e.target)+" → 吞掉(长按抬手补发的那一发,菜单保持打开)"),e.__contextjsClickSwallowed=!0,e.preventDefault(),void e.stopPropagation();x("click target="+v(e.target)+" → 关菜单 + 吞掉(state="+d+" closing="+Math.max(0,s-Date.now())+"ms)"),C("click-outside"),e.preventDefault(),e.stopPropagation()}else Date.now()-l<3e3&&x("click target="+v(e.target)+" → 放行(屏幕上没有菜单)")}}),!0),document.addEventListener("touchstart",(function(e){p=Date.now(),b()&&!k(e.target)&&(x("touch target="+v(e.target)+" → 关菜单 + 吞掉随后的 click"),C("touch-outside"),w=Date.now()+700)}),!0),document.addEventListener("touchmove",(function(){w=0}),!0),document.addEventListener("scroll",(function(e){d&&!k(e.target)&&(D()?x("scroll target="+v(e.target)+" → 忽略(菜单刚打开 "+(Date.now()-c)+"ms)"):C("scroll"))}),!0),window.addEventListener("resize",(function(){d&&!D()&&C("resize")}),!0),window.addEventListener("orientationchange",(function(){d&&C("orientationchange")}),!0),document.addEventListener("keydown",(function(e){let t=e.key||"";!d||"Escape"!==t&&"Esc"!==t&&27!==e.keyCode||C("esc")}),!0))}function S(t,n,o,i){let a=i?" dropdown-context-sub":"",r=e.compress?" compressed-context":"",d=$('<ul class="dropdown-menu dropdown-contexLine truncated
|
||||
/*
|
||||
* Context.js
|
||||
* Copyright Jacob Kelley, Modified by WispX
|
||||
* MIT License
|
||||
*
|
||||
* ===== fork 补丁(mole404/lsky-pro-docker)=====
|
||||
* 背景一:iOS 上的 WebKit 永远不会派发 contextmenu 事件(WebKit bug 213953),
|
||||
* 长按图片只会弹出系统 callout,于是"我的图片"里的自定义菜单在 iPhone/iPad 上完全不可用。
|
||||
* 处理:在**不改动原有 contextmenu 路径**的前提下,新增一条仅 iOS 生效的长按分支:
|
||||
* 1. 自己用 touchstart + 定时器识别长按,复用同一段开菜单逻辑(beforeOpen/afterOpen 照旧生效)
|
||||
* 2. 注入 -webkit-touch-callout: none 压掉 iOS 的原生菜单/放大镜(只作用于本库绑定的元素范围)
|
||||
* 3. 吞掉 iOS 抬手补发的那一发 click(否则会顺手触发图片预览等点击行为)
|
||||
*
|
||||
* 背景二(菜单收放,2026-09-28 追加):
|
||||
* 原库只在 document 的冒泡阶段 fadeOut,既不阻止事件传播;二级菜单又只靠 CSS :hover 显示。于是
|
||||
* (a) 点"菜单之外"会点穿到页面(点别的图片顺手开了预览、点链接会跳转);
|
||||
* (b) 手机上没有光标,点"复制链接"这种带二级菜单的项时,合成鼠标事件让二级菜单闪一下,
|
||||
* 紧接着那发 click 撞上关闭逻辑,整个菜单直接消失。
|
||||
* 处理:(a) 在 document 的**捕获阶段**装一层菜单守卫 —— 只关菜单、并吞掉这一发事件;
|
||||
* (b) 触摸设备((hover: none))改成点击展开/收起二级菜单,有鼠标的设备继续用 hover;
|
||||
* 另外支持滚动/缩放/Esc 关闭菜单(都是老师要的"收得优雅")。
|
||||
*
|
||||
* 隔离性:长按分支只在 iPad/iPhone/iPod(含 iPadOS 伪装成 Mac)上返回真,Android/Windows 继续走
|
||||
* contextmenu 原路径,长按相关代码一行都不执行。菜单守卫是通用行为,但其中"点击展开二级菜单"
|
||||
* 只在没有鼠标/触摸板的设备上生效,桌面端交互与上游一致。
|
||||
*/
|
||||
window.context = window.context || (function () {
|
||||
|
||||
const LONG_PRESS_DELAY = 250; // 长按判定阈值(毫秒)。iOS 原生 callout 约 500ms,这里减半更快响应;
|
||||
// 手指移动超过 LONG_PRESS_MOVE 即取消,用来抵消阈值变短带来的误触风险
|
||||
const LONG_PRESS_MOVE = 10; // 手指移动超过这个像素数就取消长按,让位给滚动/框选
|
||||
const DEDUPE_WINDOW = 700; // 长按已开菜单后,忽略紧随其后的 contextmenu(防将来 iOS 支持该事件后弹两次)
|
||||
const NEXT_CLICK_WINDOW = 700; // 长按之后要吞掉的第一发 click
|
||||
const MENU_CLOSE_CLICK_WINDOW = 700; // 因"点了菜单外"而关菜单时,要吞掉随后那一发 click 的时间窗
|
||||
const MENU_OPEN_GRACE = 800; // 菜单刚打开的这一小段时间里,抬手补发的 click 不许把菜单关掉
|
||||
// (iOS/Android 长按抬手都可能补发一发 click,目标正是刚被长按的那张图;
|
||||
// 菜单是手指还按着时就弹出来的,抬手到补发 click 的延迟通常 <500ms,
|
||||
// 所以留 800ms 余量。真手指再点一次必定先有 touchstart,那条路径另有处理)
|
||||
const MENU_OPEN_IGNORE_INPUT = 300; // 菜单刚打开的这一小段时间里,忽略 scroll/resize/orientation。
|
||||
// 为什么需要它:菜单是绝对定位插进 body 的,**插入这个动作本身**就可能
|
||||
// 让页面高度/滚动条变化,触发浏览器自己的滚动(滚动锚定、图片懒加载重排、
|
||||
// justified-gallery 重新布局)。这些“非用户操作”的滚动会把菜单刚开就关掉,
|
||||
// 而关掉之后元素还要淡出,屏幕上看着菜单还在 —— 那一瞬间的点击就会点穿。
|
||||
const MENU_FADE_GUARD = 50; // 关菜单后,元素还要淡出 fadeSpeed 毫秒;这段时间它在屏幕上,
|
||||
// 点击必须照样被吞(否则就是“看得见菜单却点穿了”)。这里再加一点余量。
|
||||
const DEBUG_BUFFER_SIZE = 240; // 诊断环缓冲长度(context.debugDump() 用)
|
||||
|
||||
let options = {
|
||||
fadeSpeed: 100,
|
||||
filter: function ($obj) {
|
||||
// Modify $obj, Do not return
|
||||
},
|
||||
above: 'auto',
|
||||
preventDoubleContext: true,
|
||||
compress: false
|
||||
};
|
||||
|
||||
// ===== fork 补丁:iOS 长按支持(开始)=====
|
||||
|
||||
// 是否 iOS/iPadOS 上的 WebKit。注意只判 iOS 不判浏览器:iOS 上 Chrome/Edge/Firefox 内核同样是 WebKit。
|
||||
// iPadOS 13+ 的"请求桌面网站"会把 UA 伪装成 Mac(platform === 'MacIntel'),
|
||||
// 而真 Mac —— 包括苹果 M 系列 —— 的 maxTouchPoints 是 0,据此区分,不会误判。
|
||||
function isIOSWebKit() {
|
||||
if (! ('ontouchstart' in window)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return /iPad|iPhone|iPod/.test(navigator.userAgent)
|
||||
|| (navigator.platform === 'MacIntel' && navigator.maxTouchPoints > 1);
|
||||
}
|
||||
|
||||
let touchSelectors = []; // 已经接上长按分支的选择器
|
||||
let stylesInjected = false;
|
||||
let swipeClickUntil = 0; // 需要吞掉 click 的时间窗
|
||||
let lastOpenAt = 0; // 上一次由长按打开菜单的时间(用于与 contextmenu 去重)
|
||||
let guardInstalled = false;
|
||||
|
||||
// 压掉 iOS 的原生长按菜单与放大镜。只作用于本库绑定的元素范围 + 菜单本身,
|
||||
// 不碰页面其它地方的链接/文字,保证其它长按能力不受影响。
|
||||
function injectTouchStyles() {
|
||||
if (stylesInjected) {
|
||||
return;
|
||||
}
|
||||
stylesInjected = true;
|
||||
|
||||
let rules = touchSelectors.map(function (selector) {
|
||||
return selector + ', ' + selector + ' *';
|
||||
}).join(', ') + ', .dropdown-context, .dropdown-context *';
|
||||
|
||||
$('<style id="context-js-ios-touch">').appendTo('head').text(
|
||||
rules + ' { -webkit-touch-callout: none; -webkit-user-select: none; user-select: none; }'
|
||||
);
|
||||
}
|
||||
|
||||
// 捕获阶段吞掉长按之后 iOS 补发的那一发 click。
|
||||
// 用 preventDefault + stopPropagation:既不触发元素自身的点击行为(例如打开图片预览),
|
||||
// 也确认长按手势已经结束。同时在事件上打标,菜单守卫据此知道"这一发已经处理过了"。
|
||||
function installClickGuard() {
|
||||
if (guardInstalled) {
|
||||
return;
|
||||
}
|
||||
guardInstalled = true;
|
||||
|
||||
document.addEventListener('click', function (e) {
|
||||
if (Date.now() > swipeClickUntil) {
|
||||
return;
|
||||
}
|
||||
swipeClickUntil = 0;
|
||||
|
||||
for (let i = 0; i < touchSelectors.length; i++) {
|
||||
if (e.target.closest && e.target.closest(touchSelectors[i])) {
|
||||
e.__contextjsClickSwallowed = true;
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
return;
|
||||
}
|
||||
}
|
||||
}, true);
|
||||
}
|
||||
|
||||
// 给某个选择器接上长按识别。仅 iOS 调用。
|
||||
function installLongPress(selector, openMenu) {
|
||||
injectTouchStyles();
|
||||
installClickGuard();
|
||||
|
||||
let timer = null,
|
||||
startX = 0,
|
||||
startY = 0,
|
||||
triggered = false;
|
||||
|
||||
function cancel() {
|
||||
if (timer !== null) {
|
||||
clearTimeout(timer);
|
||||
timer = null;
|
||||
}
|
||||
}
|
||||
|
||||
$(document).on('touchstart', selector, function (e) {
|
||||
let nativeEvent = e.originalEvent,
|
||||
touch = nativeEvent.touches[0];
|
||||
|
||||
cancel();
|
||||
triggered = false;
|
||||
|
||||
// 多指(缩放等)不参与长按
|
||||
if (! touch || nativeEvent.touches.length > 1) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 与桌面端 stopPropagation 的语义保持一致:一次长按只交给最内层(最具体)的那个
|
||||
// attach 处理。jQuery 的委托分发是从目标向上逐层调用,所以这里是"先到先得",
|
||||
// 外层容器(例如 #images-scroll 的"刷新"菜单)不会再抢走图片自己的菜单。
|
||||
if (nativeEvent.__contextjsLongPressTaken) {
|
||||
return;
|
||||
}
|
||||
nativeEvent.__contextjsLongPressTaken = true;
|
||||
|
||||
startX = touch.clientX;
|
||||
startY = touch.clientY;
|
||||
|
||||
let item = this;
|
||||
timer = setTimeout(function () {
|
||||
timer = null;
|
||||
triggered = true;
|
||||
lastOpenAt = Date.now();
|
||||
openMenu(item, touch.clientX, touch.clientY);
|
||||
}, LONG_PRESS_DELAY);
|
||||
});
|
||||
|
||||
$(document).on('touchmove', selector, function (e) {
|
||||
let touch = e.originalEvent.touches[0];
|
||||
|
||||
if (timer === null || ! touch) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (Math.abs(touch.clientX - startX) > LONG_PRESS_MOVE
|
||||
|| Math.abs(touch.clientY - startY) > LONG_PRESS_MOVE) {
|
||||
cancel();
|
||||
}
|
||||
});
|
||||
|
||||
$(document).on('touchend touchcancel', selector, function (e) {
|
||||
cancel();
|
||||
|
||||
if (triggered) {
|
||||
swipeClickUntil = Date.now() + NEXT_CLICK_WINDOW;
|
||||
e.preventDefault();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// ===== fork 补丁:iOS 长按支持(结束)=====
|
||||
|
||||
// ===== fork 补丁:菜单收放 + 触摸端二级菜单(开始)=====
|
||||
|
||||
let menuVisible = false; // 菜单是否打开(状态)。**“拦不拦这一发点击”不能只看它**:
|
||||
// scroll/resize/Esc/淡出等多条路径都会把它提前置 false,而菜单可能还在屏幕上。
|
||||
// 一律走 menuOnScreen(),那里以“真实可见性”为准。
|
||||
let menuOpenedAt = 0; // 菜单打开时刻,用于 MENU_OPEN_GRACE / MENU_OPEN_IGNORE_INPUT
|
||||
let menuClosingUntil = 0; // 关菜单后到这一刻为止,元素仍在淡出、还在屏幕上
|
||||
let lastMenuEventAt = 0; // 最近一次菜单相关事件的时刻(诊断用:只记“与菜单有关”的点击)
|
||||
let debugBuffer = []; // 诊断环缓冲
|
||||
let debugToConsole = false; // context.debug = true 时实时打控制台
|
||||
let lastTouchAt = 0; // 最近一次触摸开始时刻:用来区分"触摸补发的 click"和"真鼠标点击"
|
||||
let suppressClickUntil = 0; // 因"点了菜单外"而关菜单后,要吞掉的那一发 click
|
||||
let menuStylesInjected = false;
|
||||
let menuGuardInstalled = false;
|
||||
|
||||
// 有鼠标/触摸板的设备((hover: none) 为假)继续用 hover 展开二级菜单;手机/平板改成点击展开。
|
||||
// jsdom 之类没有 matchMedia 的环境,退回"有没有触摸能力"判定。
|
||||
function isTouchInput() {
|
||||
if (window.matchMedia) {
|
||||
return !! window.matchMedia('(hover: none)').matches;
|
||||
}
|
||||
return ('ontouchstart' in window);
|
||||
}
|
||||
|
||||
function injectMenuStyles() {
|
||||
if (menuStylesInjected) {
|
||||
return;
|
||||
}
|
||||
menuStylesInjected = true;
|
||||
|
||||
// 与主题里这条等价,只是改成用类触发(手机上没有 hover):
|
||||
// .dropdown-context .dropdown-submenu:hover>.dropdown-menu{display:block}
|
||||
// 桌面端永远不会加上这个类,所以那条路径零影响。
|
||||
$('<style id="context-js-touch-submenu">').appendTo('head').text(
|
||||
'.dropdown-context .dropdown-submenu.touch-open > .dropdown-menu { display: block; }'
|
||||
);
|
||||
}
|
||||
|
||||
// ===== 诊断(fork 新增)=====
|
||||
// 常驻记录“与菜单有关”的事件:开、关(含原因)、以及菜单牵涉到的点击判定。
|
||||
// 万一现场还出问题,老师说一句 context.debugDump() 就能把证据拿出来,不用再猜。
|
||||
function describeNode(node) {
|
||||
if (! node || ! node.tagName) {
|
||||
return String(node);
|
||||
}
|
||||
let id = node.id ? '#' + node.id : '',
|
||||
cls = (typeof node.className === 'string' && node.className.trim())
|
||||
? '.' + node.className.trim().split(/\s+/).join('.')
|
||||
: '';
|
||||
return node.tagName.toLowerCase() + id + cls;
|
||||
}
|
||||
|
||||
function logMenuEvent(text) {
|
||||
lastMenuEventAt = Date.now();
|
||||
let line = '[' + new Date().toISOString().slice(11, 23) + '] ' + text;
|
||||
debugBuffer.push(line);
|
||||
if (debugBuffer.length > DEBUG_BUFFER_SIZE) {
|
||||
debugBuffer = debugBuffer.slice(-DEBUG_BUFFER_SIZE);
|
||||
}
|
||||
if (debugToConsole && window.console && window.console.log) {
|
||||
window.console.log('[context-js] ' + line);
|
||||
}
|
||||
}
|
||||
|
||||
// 菜单到底还在不在屏幕上?判据顺序:
|
||||
// 1) 状态就是打开 → 在;
|
||||
// 2) 状态刚被置 false 但还没淡出完 → 也在(否则这段时间就是“看得见却点穿”的窗口);
|
||||
// 3) DOM 实测兜底:元素还画着(高度 > 0)就按“在”算。
|
||||
// 第 3 条是关键:真浏览器里它才是唯一可信的判据 —— 任何一条把状态提前置 false 的路径,
|
||||
// 都不该让“屏幕上明明看得见的菜单”失去拦截能力。
|
||||
function menuOnScreen() {
|
||||
if (menuVisible) {
|
||||
return true;
|
||||
}
|
||||
if (Date.now() <= menuClosingUntil) {
|
||||
return true;
|
||||
}
|
||||
|
||||
let nodes = document.querySelectorAll('.dropdown-context');
|
||||
for (let i = 0; i < nodes.length; i++) {
|
||||
let el = nodes[i];
|
||||
if (el.style && el.style.display === 'none') {
|
||||
continue;
|
||||
}
|
||||
if (typeof el.getBoundingClientRect === 'function' && el.getBoundingClientRect().height > 0) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// 菜单刚打开?用来忽略“菜单自己引发的”scroll/resize(见 MENU_OPEN_IGNORE_INPUT)
|
||||
function justOpenedMenu() {
|
||||
return (Date.now() - menuOpenedAt) < MENU_OPEN_IGNORE_INPUT;
|
||||
}
|
||||
|
||||
function closeMenus(reason) {
|
||||
if (menuVisible) {
|
||||
logMenuEvent('close reason=' + (reason || 'unknown')
|
||||
+ ' openFor=' + (Date.now() - menuOpenedAt) + 'ms');
|
||||
}
|
||||
|
||||
menuVisible = false;
|
||||
// 淡出期间元素仍在屏幕上:这段时间的点击必须照样被吞
|
||||
menuClosingUntil = Date.now() + options.fadeSpeed + MENU_FADE_GUARD;
|
||||
|
||||
$('.dropdown-context').fadeOut(options.fadeSpeed, function () {
|
||||
$('.dropdown-context').css({ display: '' });
|
||||
$('.dropdown-context .drop-left').removeClass('drop-left');
|
||||
$('.dropdown-context .touch-open').removeClass('touch-open');
|
||||
});
|
||||
}
|
||||
|
||||
function isInsideMenu(node) {
|
||||
return !! (node && node.closest && node.closest('.dropdown-context'));
|
||||
}
|
||||
|
||||
// 触摸设备:点"带二级菜单的父项" → 展开/收起它的二级菜单,且**不关闭**整个菜单。
|
||||
// 返回 true 表示这次点击已被消费(守卫不要再往下走)。
|
||||
// 注意父项本身没有 action(只是个容器),所以拦下它不会影响复制等功能:
|
||||
// 真正干活的叶子项(.copy,ClipboardJS 绑的就是它们)在二级菜单里,点击照旧放行。
|
||||
function handleSubmenuTap(e) {
|
||||
if (! isTouchInput()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
let $a = $(e.target).closest('a');
|
||||
if (! $a.length) {
|
||||
return false;
|
||||
}
|
||||
|
||||
let $li = $a.parent();
|
||||
if (! $li.length || ! $li.hasClass('dropdown-submenu')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
let wasOpen = $li.hasClass('touch-open');
|
||||
|
||||
// 同一层级只留一个展开的
|
||||
$li.siblings('.dropdown-submenu').removeClass('touch-open');
|
||||
|
||||
if (wasOpen) {
|
||||
$li.removeClass('touch-open');
|
||||
return true;
|
||||
}
|
||||
|
||||
$li.addClass('touch-open');
|
||||
|
||||
// 复用 hover 路径的溢出检测:先展开(有了尺寸)再判断要不要往左翻
|
||||
let $sub = $li.find('.dropdown-context-sub:first');
|
||||
if ($sub.length) {
|
||||
let subWidth = $sub.width(),
|
||||
subLeft = $sub.offset().left;
|
||||
|
||||
if ((subWidth + subLeft) > window.innerWidth) {
|
||||
$sub.addClass('drop-left');
|
||||
} else {
|
||||
$sub.removeClass('drop-left');
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// 菜单守卫。全部装在 document 的**捕获阶段**:jQuery 的委托与 viewer.js 都挂在冒泡阶段,
|
||||
// 捕获阶段拦下 = 这一发事件永远不会到达页面自己的处理器。
|
||||
// 它在 context.init() 时就装好(不是等第一次开菜单),因为守卫要记录"最近一次触摸开始时刻"——
|
||||
// 第一次长按的 touchstart 必须被记到,否则那次抬手补发的 click 会被误判成真点击、把菜单关掉。
|
||||
function installMenuGuard() {
|
||||
if (menuGuardInstalled) {
|
||||
return;
|
||||
}
|
||||
menuGuardInstalled = true;
|
||||
|
||||
injectMenuStyles();
|
||||
|
||||
document.addEventListener('click', function (e) {
|
||||
if (e.__contextjsClickSwallowed) {
|
||||
return; // 同一发事件已被长按分支吞过
|
||||
}
|
||||
if (Date.now() <= swipeClickUntil) {
|
||||
return; // 长按刚开过菜单,抬手那发 click 归长按分支处理
|
||||
}
|
||||
// 因"点了菜单外"而关菜单时,菜单在**手指按下**的瞬间就已经关掉了(见下面的
|
||||
// touchstart),所以这一发必须排在 menuVisible 判断之前,否则漏吞、点击会点穿到页面。
|
||||
if (Date.now() <= suppressClickUntil) {
|
||||
suppressClickUntil = 0;
|
||||
e.__contextjsClickSwallowed = true;
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
return;
|
||||
}
|
||||
|
||||
if (! menuOnScreen()) {
|
||||
// 连“刚关不久、还在淡出”都算不上 → 与菜单无关的普通点击,照旧放行
|
||||
if (Date.now() - lastMenuEventAt < 3000) {
|
||||
logMenuEvent('click target=' + describeNode(e.target) + ' → 放行(屏幕上没有菜单)');
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (isInsideMenu(e.target)) {
|
||||
logMenuEvent('click target=' + describeNode(e.target) + ' → 菜单内部,放行(交给原有逻辑)');
|
||||
handleSubmenuTap(e); // 菜单内部:只处理二级菜单的点击展开
|
||||
return; // 其它内部点击照旧(执行动作 + 原有逻辑关菜单)
|
||||
}
|
||||
|
||||
// 菜单刚打开 + 这一发 click 紧随一次触摸 → 认定是长按抬手补发的那一发 click
|
||||
// (目标正是刚被长按的图片)。吞掉它,但**不**关菜单,否则菜单会"刚开就自己关掉"。
|
||||
// 桌面鼠标点击不满足"紧随触摸"这个条件(lastTouchAt 是 0 或很久以前),照常关菜单。
|
||||
if ((Date.now() - lastTouchAt) < MENU_OPEN_GRACE
|
||||
&& (Date.now() - menuOpenedAt) < MENU_OPEN_GRACE) {
|
||||
logMenuEvent('click target=' + describeNode(e.target)
|
||||
+ ' → 吞掉(长按抬手补发的那一发,菜单保持打开)');
|
||||
e.__contextjsClickSwallowed = true;
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
return;
|
||||
}
|
||||
|
||||
logMenuEvent('click target=' + describeNode(e.target)
|
||||
+ ' → 关菜单 + 吞掉(state=' + menuVisible + ' closing='
|
||||
+ Math.max(0, menuClosingUntil - Date.now()) + 'ms)');
|
||||
|
||||
// 点击菜单之外的任何位置:只关闭菜单,绝不把这发事件放给页面
|
||||
// (否则会顺手打开图片预览、跳转链接)
|
||||
closeMenus('click-outside');
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}, true);
|
||||
|
||||
// 触摸:手指一按下就把菜单收起来,比等抬手更跟手。
|
||||
// **不拦截 touchstart 本身**(没有 preventDefault),所以页面滚动/缩放完全不受影响;
|
||||
// 随后补发的那发 click 用时间窗吞掉。
|
||||
document.addEventListener('touchstart', function (e) {
|
||||
lastTouchAt = Date.now();
|
||||
|
||||
if (! menuOnScreen() || isInsideMenu(e.target)) {
|
||||
return;
|
||||
}
|
||||
|
||||
logMenuEvent('touch target=' + describeNode(e.target) + ' → 关菜单 + 吞掉随后的 click');
|
||||
closeMenus('touch-outside');
|
||||
suppressClickUntil = Date.now() + MENU_CLOSE_CLICK_WINDOW;
|
||||
}, true);
|
||||
|
||||
// 手指动了说明是滚动/拖拽手势,别把随后那一发 click 也吞掉
|
||||
document.addEventListener('touchmove', function () {
|
||||
suppressClickUntil = 0;
|
||||
}, true);
|
||||
|
||||
// 滚动 / 旋转缩放:菜单用的是页面坐标,视口一动它就会粘在错的地方,直接收起来。
|
||||
// 监听装在 document 且用捕获,是为了连 #images-scroll 这类内部滚动容器也能收到。
|
||||
document.addEventListener('scroll', function (e) {
|
||||
if (! menuVisible || isInsideMenu(e.target)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (justOpenedMenu()) {
|
||||
// 菜单刚插进 body,浏览器自己可能来一发滚动(滚动锚定 / 懒加载重排 / 画廊重排)——
|
||||
// 这一发不是用户滚的,别拿它把菜单关掉:关掉之后元素还要淡出,
|
||||
// 屏幕上看着菜单还在,那一瞬间的点击就会点穿。
|
||||
logMenuEvent('scroll target=' + describeNode(e.target)
|
||||
+ ' → 忽略(菜单刚打开 ' + (Date.now() - menuOpenedAt) + 'ms)');
|
||||
return;
|
||||
}
|
||||
|
||||
closeMenus('scroll');
|
||||
}, true);
|
||||
|
||||
window.addEventListener('resize', function () {
|
||||
if (menuVisible && ! justOpenedMenu()) {
|
||||
closeMenus('resize');
|
||||
}
|
||||
}, true);
|
||||
|
||||
window.addEventListener('orientationchange', function () {
|
||||
if (menuVisible) {
|
||||
closeMenus('orientationchange');
|
||||
}
|
||||
}, true);
|
||||
|
||||
// 桌面端顺手:Esc 关闭(不 preventDefault,免得抢走其它 UI 的 Esc)
|
||||
document.addEventListener('keydown', function (e) {
|
||||
let key = e.key || '';
|
||||
if (menuVisible && (key === 'Escape' || key === 'Esc' || e.keyCode === 27)) {
|
||||
closeMenus('esc');
|
||||
}
|
||||
}, true);
|
||||
}
|
||||
|
||||
// ===== fork 补丁:菜单收放 + 触摸端二级菜单(结束)=====
|
||||
|
||||
function initialize(opts) {
|
||||
|
||||
options = $.extend({}, options, opts);
|
||||
|
||||
installMenuGuard();
|
||||
|
||||
$(document).on('click', 'html', function () {
|
||||
// 菜单内部的点击照旧关闭菜单(菜单外的点击在守卫里处理,事件已经被吞掉、到不了这里)
|
||||
if (menuVisible) {
|
||||
closeMenus();
|
||||
}
|
||||
});
|
||||
if (options.preventDoubleContext) {
|
||||
$(document).on('contextmenu', '.dropdown-context', function (e) {
|
||||
e.preventDefault();
|
||||
});
|
||||
}
|
||||
$(document).on('mouseenter', '.dropdown-submenu', function () {
|
||||
let $sub = $(this).find('.dropdown-context-sub:first'),
|
||||
subWidth = $sub.width(),
|
||||
subLeft = $sub.offset().left,
|
||||
collision = (subWidth + subLeft) > window.innerWidth;
|
||||
if (collision) {
|
||||
$sub.addClass('drop-left');
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
function updateOptions(opts) {
|
||||
options = $.extend({}, options, opts);
|
||||
}
|
||||
|
||||
function buildMenu(event, data, id, subMenu) {
|
||||
let subClass = (subMenu) ? ' dropdown-context-sub' : '',
|
||||
compressed = options.compress ? ' compressed-context' : '',
|
||||
$menu = $('<ul class="dropdown-menu dropdown-context' + subClass + compressed + '" id="dropdown-' + id + '"></ul>');
|
||||
let i = 0, linkTarget = '';
|
||||
for (i; i < data.length; i++) {
|
||||
if (typeof data[i].divider !== 'undefined') {
|
||||
$menu.append('<li class="divider"></li>');
|
||||
} else if (typeof data[i].header !== 'undefined') {
|
||||
$menu.append('<li class="nav-header">' + data[i].header + '</li>');
|
||||
} else {
|
||||
if (typeof data[i].href == 'undefined') {
|
||||
data[i].href = 'javascript:void(0)';
|
||||
}
|
||||
if (typeof data[i].target !== 'undefined') {
|
||||
linkTarget = ' target="' + data[i].target + '"';
|
||||
}
|
||||
let $sub;
|
||||
if (typeof data[i].subMenu !== 'undefined') {
|
||||
$sub = $('<li class="dropdown-submenu"><a tabindex="-1" href="' + data[i].href + '">' + data[i].text + '</a></li>');
|
||||
} else {
|
||||
$sub = $('<li><a tabindex="-1" href="' + data[i].href + '"' + linkTarget + '>' + data[i].text + '</a></li>');
|
||||
}
|
||||
// show or hide?
|
||||
if (typeof data[i].visible === 'function') {
|
||||
if (! data[i].visible(event)) {
|
||||
$sub.hide();
|
||||
}
|
||||
}
|
||||
let $a = $sub.find('a');
|
||||
// custom classes
|
||||
if (typeof data[i].classes !== 'undefined') {
|
||||
for (const classKey in data[i].classes) {
|
||||
$a.addClass(data[i].classes[classKey]);
|
||||
}
|
||||
}
|
||||
// custom attributes
|
||||
if (typeof data[i].attributes !== 'undefined') {
|
||||
for (const attributesKey in data[i].attributes) {
|
||||
$a.attr(attributesKey, data[i].attributes[attributesKey]);
|
||||
}
|
||||
}
|
||||
// click callback
|
||||
if (typeof data[i].action !== 'undefined') {
|
||||
let actionID = 'event-' + new Date().getTime() * Math.floor(Math.random() * 100000),
|
||||
eventAction = data[i].action;
|
||||
$a.attr('id', actionID);
|
||||
$('#' + actionID).addClass('context-event');
|
||||
$(document).on('click', '#' + actionID, function () {
|
||||
eventAction.call(this, event);
|
||||
});
|
||||
}
|
||||
$menu.append($sub);
|
||||
if (typeof data[i].subMenu != 'undefined') {
|
||||
let subMenuData = buildMenu(event, data[i].subMenu, id, true);
|
||||
$menu.find('li:last').append(subMenuData);
|
||||
}
|
||||
}
|
||||
if (typeof options.filter == 'function') {
|
||||
options.filter($menu.find('li:last'));
|
||||
}
|
||||
}
|
||||
return $menu;
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加菜单
|
||||
* @param selector 被右击元素
|
||||
* @param opts 参数 {
|
||||
* data: Array[Object] {
|
||||
* text: String, // 文本
|
||||
* classes: Array, // class
|
||||
* attributes: Object, // 属性
|
||||
* action: Function, // 点击后的回调 function (e) {}
|
||||
* visible: Function, // 函数返回bool类型,表示显示或隐藏按钮 function (e) {}
|
||||
* }
|
||||
* beforeOpen: Function, // 打开前 function (item) {}
|
||||
* afterOpen: Function, // 打开后 function (item, dropdown) {}
|
||||
* }
|
||||
*/
|
||||
function addContext(selector, opts) {
|
||||
opts = opts || {};
|
||||
let data = opts.data || {};
|
||||
let id = new Date().getTime();
|
||||
|
||||
// fork 补丁:把"打开菜单"从 contextmenu 回调里抽出来,
|
||||
// 让桌面/安卓的 contextmenu 与 iOS 的长按共用同一段逻辑。
|
||||
// item 为匹配到的元素,pageX/pageY 为菜单定位坐标,evt 用于回调里的 this(长按时为 null)。
|
||||
function openMenu(item, pageX, pageY, evt) {
|
||||
if (! item) {
|
||||
return;
|
||||
}
|
||||
|
||||
typeof opts.beforeOpen === 'function' && opts.beforeOpen.call(evt || item, item);
|
||||
|
||||
let $menu = buildMenu(item, data, id);
|
||||
// clear dropdowns
|
||||
$('body .dropdown-menu.dropdown-context').remove();
|
||||
// create dropdown
|
||||
$('body').append($menu);
|
||||
|
||||
$('.dropdown-context:not(.dropdown-context-sub)').hide();
|
||||
|
||||
let $dd = $("#dropdown-" + id);
|
||||
|
||||
if (typeof options.above == 'boolean' && options.above) {
|
||||
$dd.addClass('dropdown-context-up').css({
|
||||
top: pageY - 20 - $('#dropdown-' + id).height(),
|
||||
left: pageX - 13
|
||||
}).fadeIn(options.fadeSpeed);
|
||||
} else if (typeof options.above == 'string' && options.above === 'auto') {
|
||||
$dd.removeClass('dropdown-context-up');
|
||||
let autoH = $dd.height() + 12;
|
||||
if ((pageY + autoH) > $('html').height()) {
|
||||
$dd.addClass('dropdown-context-up').css({
|
||||
top: pageY - 20 - autoH,
|
||||
left: pageX - 13
|
||||
}).fadeIn(options.fadeSpeed);
|
||||
} else {
|
||||
$dd.css({
|
||||
top: pageY + 10,
|
||||
left: pageX - 13
|
||||
}).fadeIn(options.fadeSpeed);
|
||||
}
|
||||
}
|
||||
|
||||
menuVisible = true;
|
||||
menuOpenedAt = Date.now();
|
||||
menuClosingUntil = 0;
|
||||
|
||||
logMenuEvent('open trigger=' + (evt ? 'contextmenu' : 'longpress')
|
||||
+ ' item=' + describeNode(item)
|
||||
+ ' at=' + Math.round(pageX) + ',' + Math.round(pageY));
|
||||
|
||||
typeof opts.afterOpen === 'function' && opts.afterOpen.call(evt || item, item, $dd.get(0));
|
||||
}
|
||||
|
||||
$(document).on('contextmenu', selector, function (e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
// 长按刚开过菜单就忽略这一发(避免重复弹菜单)
|
||||
if (Date.now() - lastOpenAt < DEDUPE_WINDOW) {
|
||||
return;
|
||||
}
|
||||
|
||||
openMenu(e.target.closest(selector), e.pageX, e.pageY, e);
|
||||
});
|
||||
|
||||
// fork 补丁:iOS 上 contextmenu 永远不来,改由长按触发。仅 iOS 生效。
|
||||
if (isIOSWebKit()) {
|
||||
touchSelectors.push(selector);
|
||||
installLongPress(selector, function (item, clientX, clientY) {
|
||||
openMenu(item, clientX + window.pageXOffset, clientY + window.pageYOffset, null);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function destroyContext(selector) {
|
||||
$(document).off('contextmenu', selector).off('click', '.context-event');
|
||||
}
|
||||
|
||||
let api = {
|
||||
init: initialize,
|
||||
settings: updateOptions,
|
||||
attach: addContext,
|
||||
destroy: destroyContext,
|
||||
// fork 补丁:暴露菜单开合状态,供测试与诊断用(上游没有这个)
|
||||
isMenuOpen: function () {
|
||||
return menuVisible;
|
||||
},
|
||||
// fork 补丁:菜单此刻是否“在屏幕上”(状态 + 淡出窗口 + DOM 实测),诊断用
|
||||
isMenuOnScreen: function () {
|
||||
return menuOnScreen();
|
||||
},
|
||||
// fork 补丁:诊断转储。出问题时在控制台执行 copy(context.debugDump()) 即可复制出来。
|
||||
debugDump: function () {
|
||||
let head = [
|
||||
'# context-js 诊断转储 ' + new Date().toISOString(),
|
||||
'url: ' + window.location.href,
|
||||
'ua: ' + navigator.userAgent,
|
||||
'fadeSpeed: ' + options.fadeSpeed + ' | 触摸输入(hover:none): ' + isTouchInput(),
|
||||
'menuVisible: ' + menuVisible + ' | menuOnScreen: ' + menuOnScreen()
|
||||
+ ' | 淡出剩余: ' + Math.max(0, menuClosingUntil - Date.now()) + 'ms',
|
||||
'--- 事件(时间升序,最多 ' + DEBUG_BUFFER_SIZE + ' 条)---'
|
||||
];
|
||||
return head.concat(debugBuffer).join('\n');
|
||||
}
|
||||
};
|
||||
|
||||
Object.defineProperty(api, 'debug', {
|
||||
get: function () {
|
||||
return debugToConsole;
|
||||
},
|
||||
set: function (v) {
|
||||
debugToConsole = !! v;
|
||||
logMenuEvent('debug 控制台实时输出 = ' + debugToConsole);
|
||||
}
|
||||
});
|
||||
|
||||
return api;
|
||||
})();
|
||||
@@ -16,7 +16,6 @@
|
||||
"/js/clipboard/clipboard.min.js": "/js/clipboard/clipboard.min.js",
|
||||
"/js/clipboard/index.browser.js": "/js/clipboard/index.browser.js",
|
||||
"/js/dragselect/ds.min.js": "/js/dragselect/ds.min.js",
|
||||
"/js/context-js/context-js.js": "/js/context-js/context-js.js",
|
||||
"/js/echarts/echarts.min.js": "/js/echarts/echarts.min.js",
|
||||
"/js/masonry/masonry.pkgd.min.js": "/js/masonry/masonry.pkgd.min.js",
|
||||
"/js/imagesloaded/imagesloaded.pkgd.min.js": "/js/imagesloaded/imagesloaded.pkgd.min.js",
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
/*
|
||||
* 构建收尾:把「打过补丁、且在 Dockerfile 里按 md5 + 关键标记自证」的资源按原字节拷进 public/。
|
||||
*
|
||||
* 为什么不用 webpack 的 mix.copy:
|
||||
* laravel-mix 在 --production 下会对 copy 过去的文件做压缩改名。我们的补丁文件
|
||||
* (resources/js/context-js.js,iOS 长按菜单)在镜像构建时按 md5 与关键标记 grep 自证,
|
||||
* 被压缩后标记消失、md5 变化 → 镜像构建直接失败。
|
||||
* 这里用字节级拷贝,保证 public/js 与 resources/js 两份永远一致。
|
||||
*/
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const pairs = [
|
||||
['resources/js/context-js.js', 'public/js/context-js/context-js.js'],
|
||||
];
|
||||
|
||||
pairs.forEach(function (pair) {
|
||||
const src = path.resolve(__dirname, '..', pair[0]);
|
||||
const dst = path.resolve(__dirname, '..', pair[1]);
|
||||
fs.mkdirSync(path.dirname(dst), { recursive: true });
|
||||
fs.copyFileSync(src, dst);
|
||||
console.log('[copy-patched-assets] ' + pair[0] + ' -> ' + pair[1]);
|
||||
});
|
||||
+5
-1
@@ -42,7 +42,11 @@ mix.copy('node_modules/dragselect/dist/ds.min.js', 'public/js/dragselect')
|
||||
|
||||
// context-menu.min.js
|
||||
mix.less('resources/css/context-js.less', 'public/css/context-js')
|
||||
mix.copy('resources/js/context-js.js', 'public/js/context-js')
|
||||
// 注意:这一份【不能】用 mix.copy —— --production 下 webpack 会对 copy 过去的文件做压缩改名,
|
||||
// 结果 public/js 里的补丁版被 terser 处理(isIOSWebKit / LONG_PRESS_DELAY = 250 / installMenuGuard
|
||||
// 这些标记全丢),既让 Dockerfile 的补丁自证 grep 失败,也与 resources/js 那份不再字节一致。
|
||||
// 改由 npm run prod 收尾做字节级拷贝(scripts/copy-patched-assets.js),保证两份始终一致。
|
||||
// mix.copy('resources/js/context-js.js', 'public/js/context-js')
|
||||
|
||||
// apache echarts
|
||||
mix.copy('node_modules/echarts/dist/echarts.min.js', 'public/js/echarts')
|
||||
|
||||
Reference in new issue
Block a user