plusui-native-core 0.1.99 → 0.1.101

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -58,11 +58,31 @@ export function createDropZone(name: string, el?: HTMLElement | null): DropZone
58
58
 
59
59
  if (element) {
60
60
  element.setAttribute('data-dropzone', name);
61
+
62
+ // Prevent browser default on drop (file navigation)
63
+ // The C++ backend handles actual file delivery via WM_DROPFILES
61
64
  element.addEventListener('drop', (e: DragEvent) => {
62
65
  e.preventDefault();
63
66
  e.stopPropagation();
64
67
  element.classList.remove('dropzone-active');
65
68
  });
69
+
70
+ // Visual feedback: the injected C++ script manages dropzone-active
71
+ // via document-level listeners, but we also handle cleanup here
72
+ element.addEventListener('dragleave', (e: DragEvent) => {
73
+ // Only remove if actually leaving the element (not entering a child)
74
+ const related = e.relatedTarget as Node | null;
75
+ if (!related || !element.contains(related)) {
76
+ element.classList.remove('dropzone-active');
77
+ }
78
+ });
79
+
80
+ element.addEventListener('dragover', (e: DragEvent) => {
81
+ e.preventDefault();
82
+ if (e.dataTransfer) {
83
+ try { e.dataTransfer.dropEffect = 'copy'; } catch (_) {}
84
+ }
85
+ });
66
86
  }
67
87
 
68
88
  return {
@@ -123,36 +123,75 @@ Window Window::create(void *windowHandle, const WindowConfig &config) {
123
123
  nullptr);
124
124
  }
125
125
 
126
- // Disable webview drag & drop behavior if requested
127
- // This prevents files from being loaded in the browser
128
- // and allows the native FileDrop API to handle them instead
129
- if (pImpl->config.disableWebviewDragDrop) {
130
- std::string disableDragDropScript = R"(
131
- (function() {
132
- // Prevent default drag and drop behavior on document/window
133
- ['dragenter', 'dragover', 'dragleave', 'drop'].forEach(function(eventName) {
134
- document.addEventListener(eventName, function(e) {
135
- // Only prevent default on document.body or document.documentElement
136
- // to allow custom drop zones to still work
137
- if (e.target === document.body || e.target === document.documentElement || e.target === document) {
138
- e.preventDefault();
139
- e.stopPropagation();
140
- }
141
- }, true);
142
-
143
- window.addEventListener(eventName, function(e) {
144
- e.preventDefault();
145
- e.stopPropagation();
146
- }, true);
147
- });
148
- })();
149
- )";
150
- pImpl->webview->AddScriptToExecuteOnDocumentCreated(
151
- std::wstring(disableDragDropScript.begin(),
152
- disableDragDropScript.end())
153
- .c_str(),
154
- nullptr);
155
- }
126
+ // Block browser default drag-drop behavior (prevents
127
+ // the browser from navigating to the dropped file)
128
+ // while still showing visual feedback on drop zones.
129
+ // File delivery is handled by WM_DROPFILES in wndProc.
130
+ if (pImpl->config.disableWebviewDragDrop ||
131
+ pImpl->config.enableFileDrop) {
132
+ std::string disableDragDropScript = R"(
133
+ (function() {
134
+ if (window.__plusui_dropzone_init) return;
135
+ window.__plusui_dropzone_init = true;
136
+
137
+ var activeZone = null;
138
+
139
+ var findDropZone = function(e) {
140
+ var target = null;
141
+ if (e && typeof e.clientX === 'number' && document.elementFromPoint) {
142
+ target = document.elementFromPoint(e.clientX, e.clientY);
143
+ }
144
+ if (!target && e && e.target) target = e.target;
145
+ if (!target || !target.closest) return null;
146
+ return target.closest('[data-dropzone]');
147
+ };
148
+
149
+ var updateActiveZone = function(zone) {
150
+ if (activeZone === zone) return;
151
+ if (activeZone) activeZone.classList.remove('dropzone-active');
152
+ activeZone = zone;
153
+ if (activeZone) activeZone.classList.add('dropzone-active');
154
+ };
155
+
156
+ document.addEventListener('dragenter', function(e) {
157
+ e.preventDefault();
158
+ var zone = findDropZone(e);
159
+ updateActiveZone(zone);
160
+ if (e.dataTransfer) {
161
+ try { e.dataTransfer.dropEffect = zone ? 'copy' : 'none'; } catch(_) {}
162
+ }
163
+ }, true);
164
+
165
+ document.addEventListener('dragover', function(e) {
166
+ e.preventDefault();
167
+ var zone = findDropZone(e);
168
+ updateActiveZone(zone);
169
+ if (e.dataTransfer) {
170
+ try { e.dataTransfer.dropEffect = zone ? 'copy' : 'none'; } catch(_) {}
171
+ }
172
+ }, true);
173
+
174
+ document.addEventListener('dragleave', function(e) {
175
+ e.preventDefault();
176
+ var zone = findDropZone(e);
177
+ updateActiveZone(zone);
178
+ }, true);
179
+
180
+ document.addEventListener('drop', function(e) {
181
+ e.preventDefault();
182
+ updateActiveZone(null);
183
+ }, true);
184
+
185
+ window.addEventListener('dragover', function(e) { e.preventDefault(); }, true);
186
+ window.addEventListener('drop', function(e) { e.preventDefault(); }, true);
187
+ })();
188
+ )";
189
+ pImpl->webview->AddScriptToExecuteOnDocumentCreated(
190
+ std::wstring(disableDragDropScript.begin(),
191
+ disableDragDropScript.end())
192
+ .c_str(),
193
+ nullptr);
194
+ }
156
195
 
157
196
  // Set up WebMessageReceived handler for JS->C++ bridge
158
197
  pImpl->webview->add_WebMessageReceived(
@@ -519,32 +558,30 @@ Window Window::create(void *windowHandle, const WindowConfig &config) {
519
558
  << "[PlusUI] Unknown menu method: "
520
559
  << menuMethod << std::endl;
521
560
  }
522
- } else if (method.find("webview.") == 0) {
523
- // WebView API routing
524
- std::string webviewMethod = method.substr(8);
525
- if (webviewMethod == "setWebviewDragDropEnabled") {
526
- size_t p1 = msg.find("[");
527
- size_t p2 = msg.find("]");
528
- if (p1 != std::string::npos &&
529
- p2 != std::string::npos) {
530
- std::string params =
531
- msg.substr(p1 + 1, p2 - p1 - 1);
532
- bool enabled = params.find("true") != std::string::npos;
533
- // Note: Need to call this on the WebView instance
534
- // This is a placeholder - actual implementation needs WebView reference
535
- // For now, execute script directly
536
- std::string script;
537
- if (enabled) {
538
- script = "(function() { if (window.__plusui_dragDropDisabled) { delete window.__plusui_dragDropDisabled; } })();";
539
- } else {
540
- script = "(function() { if (window.__plusui_dragDropDisabled) return; window.__plusui_dragDropDisabled = true; ['dragenter', 'dragover', 'dragleave', 'drop'].forEach(function(eventName) { document.addEventListener(eventName, function(e) { if (e.target === document.body || e.target === document.documentElement || e.target === document) { e.preventDefault(); e.stopPropagation(); } }, true); window.addEventListener(eventName, function(e) { e.preventDefault(); e.stopPropagation(); }, true); }); })();";
541
- }
542
- pImpl->webview->ExecuteScript(
543
- std::wstring(script.begin(), script.end()).c_str(),
544
- nullptr);
545
- }
546
- success = true;
547
- }
561
+ } else if (method.find("webview.") == 0) {
562
+ // WebView API routing
563
+ std::string webviewMethod = method.substr(8);
564
+ if (webviewMethod == "setWebviewDragDropEnabled") {
565
+ size_t p1 = msg.find("[");
566
+ size_t p2 = msg.find("]");
567
+ if (p1 != std::string::npos &&
568
+ p2 != std::string::npos) {
569
+ std::string params =
570
+ msg.substr(p1 + 1, p2 - p1 - 1);
571
+ bool enabled = params.find("true") != std::string::npos;
572
+ std::string script;
573
+ if (enabled) {
574
+ script = "(function() { delete window.__plusui_dropzone_init; })();";
575
+ } else {
576
+ // Re-install the dropzone blocker script
577
+ script = "(function() { if (window.__plusui_dropzone_init) return; window.__plusui_dropzone_init = true; var activeZone = null; var findDropZone = function(e) { var target = null; if (e && typeof e.clientX === 'number' && document.elementFromPoint) { target = document.elementFromPoint(e.clientX, e.clientY); } if (!target && e && e.target) target = e.target; if (!target || !target.closest) return null; return target.closest('[data-dropzone]'); }; var updateActiveZone = function(zone) { if (activeZone === zone) return; if (activeZone) activeZone.classList.remove('dropzone-active'); activeZone = zone; if (activeZone) activeZone.classList.add('dropzone-active'); }; document.addEventListener('dragenter', function(e) { e.preventDefault(); var zone = findDropZone(e); updateActiveZone(zone); if (e.dataTransfer) { try { e.dataTransfer.dropEffect = zone ? 'copy' : 'none'; } catch(_) {} } }, true); document.addEventListener('dragover', function(e) { e.preventDefault(); var zone = findDropZone(e); updateActiveZone(zone); if (e.dataTransfer) { try { e.dataTransfer.dropEffect = zone ? 'copy' : 'none'; } catch(_) {} } }, true); document.addEventListener('dragleave', function(e) { e.preventDefault(); var zone = findDropZone(e); updateActiveZone(zone); }, true); document.addEventListener('drop', function(e) { e.preventDefault(); updateActiveZone(null); }, true); window.addEventListener('dragover', function(e) { e.preventDefault(); }, true); window.addEventListener('drop', function(e) { e.preventDefault(); }, true); })();";
578
+ }
579
+ pImpl->webview->ExecuteScript(
580
+ std::wstring(script.begin(), script.end()).c_str(),
581
+ nullptr);
582
+ }
583
+ success = true;
584
+ }
548
585
  } else if (method.find("webgpu.") == 0) {
549
586
  // WebGPU API routing
550
587
  std::string webgpuMethod = method.substr(
@@ -661,29 +698,60 @@ Window Window::create(void *windowHandle, const WindowConfig &config) {
661
698
  [config.preferences setValue:@YES forKey:@"developerExtrasEnabled"];
662
699
  }
663
700
 
664
- // Disable webview drag & drop behavior to allow native FileDrop API
665
- if (win.pImpl->config.disableWebviewDragDrop) {
666
- NSString *disableDragDropScript = @"(function() {"
667
- "['dragenter', 'dragover', 'dragleave', 'drop'].forEach(function(eventName) {"
668
- "document.addEventListener(eventName, function(e) {"
669
- "if (e.target === document.body || e.target === document.documentElement || e.target === document) {"
670
- "e.preventDefault();"
671
- "e.stopPropagation();"
672
- "}"
673
- "}, true);"
674
- "window.addEventListener(eventName, function(e) {"
675
- "e.preventDefault();"
676
- "e.stopPropagation();"
677
- "}, true);"
678
- "});"
679
- "})();";
680
-
681
- WKUserScript *userScript = [[WKUserScript alloc]
682
- initWithSource:disableDragDropScript
683
- injectionTime:WKUserScriptInjectionTimeAtDocumentEnd
684
- forMainFrameOnly:NO];
685
- [config.userContentController addUserScript:userScript];
686
- }
701
+ // Block browser default drag-drop while allowing drop zone visual feedback.
702
+ // File delivery is handled natively by macOS drag APIs, not browser events.
703
+ if (win.pImpl->config.disableWebviewDragDrop ||
704
+ win.pImpl->config.enableFileDrop) {
705
+ NSString *disableDragDropScript = @"(function() {"
706
+ "if (window.__plusui_dropzone_init) return;"
707
+ "window.__plusui_dropzone_init = true;"
708
+ "var activeZone = null;"
709
+ "var findDropZone = function(e) {"
710
+ "var target = null;"
711
+ "if (e && typeof e.clientX === 'number' && document.elementFromPoint) {"
712
+ "target = document.elementFromPoint(e.clientX, e.clientY);"
713
+ "}"
714
+ "if (!target && e && e.target) target = e.target;"
715
+ "if (!target || !target.closest) return null;"
716
+ "return target.closest('[data-dropzone]');"
717
+ "};"
718
+ "var updateActiveZone = function(zone) {"
719
+ "if (activeZone === zone) return;"
720
+ "if (activeZone) activeZone.classList.remove('dropzone-active');"
721
+ "activeZone = zone;"
722
+ "if (activeZone) activeZone.classList.add('dropzone-active');"
723
+ "};"
724
+ "document.addEventListener('dragenter', function(e) {"
725
+ "e.preventDefault();"
726
+ "var zone = findDropZone(e);"
727
+ "updateActiveZone(zone);"
728
+ "if (e.dataTransfer) { try { e.dataTransfer.dropEffect = zone ? 'copy' : 'none'; } catch(_) {} }"
729
+ "}, true);"
730
+ "document.addEventListener('dragover', function(e) {"
731
+ "e.preventDefault();"
732
+ "var zone = findDropZone(e);"
733
+ "updateActiveZone(zone);"
734
+ "if (e.dataTransfer) { try { e.dataTransfer.dropEffect = zone ? 'copy' : 'none'; } catch(_) {} }"
735
+ "}, true);"
736
+ "document.addEventListener('dragleave', function(e) {"
737
+ "e.preventDefault();"
738
+ "var zone = findDropZone(e);"
739
+ "updateActiveZone(zone);"
740
+ "}, true);"
741
+ "document.addEventListener('drop', function(e) {"
742
+ "e.preventDefault();"
743
+ "updateActiveZone(null);"
744
+ "}, true);"
745
+ "window.addEventListener('dragover', function(e) { e.preventDefault(); }, true);"
746
+ "window.addEventListener('drop', function(e) { e.preventDefault(); }, true);"
747
+ "})();";
748
+
749
+ WKUserScript *userScript = [[WKUserScript alloc]
750
+ initWithSource:disableDragDropScript
751
+ injectionTime:WKUserScriptInjectionTimeAtDocumentEnd
752
+ forMainFrameOnly:NO];
753
+ [config.userContentController addUserScript:userScript];
754
+ }
687
755
 
688
756
  // Hide scrollbars if disabled
689
757
  if (!win.pImpl->config.scrollbars) {
@@ -1104,44 +1172,73 @@ void Window::injectCSS(const std::string &css) {
1104
1172
  executeScript(script);
1105
1173
  }
1106
1174
 
1107
- void Window::setWebviewDragDropEnabled(bool enabled) {
1108
- std::string script;
1109
-
1110
- if (enabled) {
1111
- // Enable webview drag-drop by removing our prevention handlers
1112
- script = R"(
1113
- (function() {
1114
- if (window.__plusui_dragDropDisabled) {
1115
- delete window.__plusui_dragDropDisabled;
1116
- }
1117
- })();
1118
- )";
1119
- } else {
1120
- // Disable webview drag-drop by preventing default behavior
1121
- script = R"(
1122
- (function() {
1123
- if (window.__plusui_dragDropDisabled) return; // Already disabled
1124
- window.__plusui_dragDropDisabled = true;
1125
-
1126
- ['dragenter', 'dragover', 'dragleave', 'drop'].forEach(function(eventName) {
1127
- document.addEventListener(eventName, function(e) {
1128
- if (e.target === document.body || e.target === document.documentElement || e.target === document) {
1129
- e.preventDefault();
1130
- e.stopPropagation();
1131
- }
1132
- }, true);
1133
-
1134
- window.addEventListener(eventName, function(e) {
1135
- e.preventDefault();
1136
- e.stopPropagation();
1137
- }, true);
1138
- });
1139
- })();
1140
- )";
1141
- }
1142
-
1143
- executeScript(script);
1144
- }
1175
+ void Window::setWebviewDragDropEnabled(bool enabled) {
1176
+ if (enabled) {
1177
+ executeScript(R"(
1178
+ (function() {
1179
+ delete window.__plusui_dropzone_init;
1180
+ })();
1181
+ )");
1182
+ } else {
1183
+ executeScript(R"(
1184
+ (function() {
1185
+ if (window.__plusui_dropzone_init) return;
1186
+ window.__plusui_dropzone_init = true;
1187
+
1188
+ var activeZone = null;
1189
+
1190
+ var findDropZone = function(e) {
1191
+ var target = null;
1192
+ if (e && typeof e.clientX === 'number' && document.elementFromPoint) {
1193
+ target = document.elementFromPoint(e.clientX, e.clientY);
1194
+ }
1195
+ if (!target && e && e.target) target = e.target;
1196
+ if (!target || !target.closest) return null;
1197
+ return target.closest('[data-dropzone]');
1198
+ };
1199
+
1200
+ var updateActiveZone = function(zone) {
1201
+ if (activeZone === zone) return;
1202
+ if (activeZone) activeZone.classList.remove('dropzone-active');
1203
+ activeZone = zone;
1204
+ if (activeZone) activeZone.classList.add('dropzone-active');
1205
+ };
1206
+
1207
+ document.addEventListener('dragenter', function(e) {
1208
+ e.preventDefault();
1209
+ var zone = findDropZone(e);
1210
+ updateActiveZone(zone);
1211
+ if (e.dataTransfer) {
1212
+ try { e.dataTransfer.dropEffect = zone ? 'copy' : 'none'; } catch(_) {}
1213
+ }
1214
+ }, true);
1215
+
1216
+ document.addEventListener('dragover', function(e) {
1217
+ e.preventDefault();
1218
+ var zone = findDropZone(e);
1219
+ updateActiveZone(zone);
1220
+ if (e.dataTransfer) {
1221
+ try { e.dataTransfer.dropEffect = zone ? 'copy' : 'none'; } catch(_) {}
1222
+ }
1223
+ }, true);
1224
+
1225
+ document.addEventListener('dragleave', function(e) {
1226
+ e.preventDefault();
1227
+ var zone = findDropZone(e);
1228
+ updateActiveZone(zone);
1229
+ }, true);
1230
+
1231
+ document.addEventListener('drop', function(e) {
1232
+ e.preventDefault();
1233
+ updateActiveZone(null);
1234
+ }, true);
1235
+
1236
+ window.addEventListener('dragover', function(e) { e.preventDefault(); }, true);
1237
+ window.addEventListener('drop', function(e) { e.preventDefault(); }, true);
1238
+ })();
1239
+ )");
1240
+ }
1241
+ }
1145
1242
 
1146
1243
  void Window::onNavigationStart(NavigationCallback callback) {
1147
1244
  pImpl->navigationCallback = callback;
@@ -244,58 +244,56 @@ struct Window::Impl {
244
244
  jsonEscape(mimeTypeFromPath(path)) +
245
245
  "\",\"size\":" + std::to_string(sizeBytes) + "}";
246
246
  }
247
- filesJson += "]";
248
-
249
- DragFinish(hDrop);
250
-
251
- POINT dropPoint = {0, 0};
252
- DragQueryPoint(hDrop, &dropPoint);
253
-
254
- std::string zoneHitScript =
255
- "(function(){try{"
256
- "var el=document.elementFromPoint(" +
257
- std::to_string(dropPoint.x) + "," + std::to_string(dropPoint.y) +
258
- ");"
259
- "if(!el) return false;"
260
- "if(!el.closest) return false;"
261
- "return !!el.closest('[data-dropzone=\\\"true\\\"], "
262
- ".filedrop-zone, .dropzone');"
263
- "}catch(_){return false;}})();";
264
-
265
- targetImpl->webview->ExecuteScript(
266
- std::wstring(zoneHitScript.begin(), zoneHitScript.end()).c_str(),
267
- Callback<ICoreWebView2ExecuteScriptCompletedHandler>(
268
- [targetImpl, filesJson](HRESULT errorCode,
269
- LPCWSTR resultJson) -> HRESULT {
270
- if (FAILED(errorCode) || !targetImpl || !targetImpl->webview)
271
- return S_OK;
272
-
273
- bool overDropZone = false;
274
- if (resultJson) {
275
- std::wstring wres(resultJson);
276
- overDropZone = (wres.find(L"true") != std::wstring::npos);
277
- }
278
- if (!overDropZone)
279
- return S_OK;
280
-
281
- std::string eventScript =
282
- "window.dispatchEvent(new "
283
- "CustomEvent('plusui:fileDrop.filesDropped',"
284
- " { detail: { files: " +
285
- filesJson + " } }));";
286
-
287
- if (targetImpl->fileDropCallback) {
288
- targetImpl->fileDropCallback(filesJson);
289
- }
290
-
291
- targetImpl->webview->ExecuteScript(
292
- std::wstring(eventScript.begin(), eventScript.end())
293
- .c_str(),
294
- nullptr);
295
- return S_OK;
296
- })
297
- .Get());
298
- return 0;
247
+ filesJson += "]";
248
+
249
+ // Must query drop point BEFORE DragFinish invalidates hDrop
250
+ POINT dropPoint = {0, 0};
251
+ DragQueryPoint(hDrop, &dropPoint);
252
+
253
+ DragFinish(hDrop);
254
+
255
+ // Always fire the C++ callback regardless of zone
256
+ if (targetImpl->fileDropCallback) {
257
+ targetImpl->fileDropCallback(filesJson);
258
+ }
259
+
260
+ // Build the event script that dispatches to the frontend.
261
+ // We always fire the global CustomEvent so any listener can react.
262
+ // If the drop lands on a [data-dropzone] element we also call the
263
+ // zone-specific callback (__plusui_fileDrop__).
264
+ int dpx = dropPoint.x;
265
+ int dpy = dropPoint.y;
266
+
267
+ std::string eventScript =
268
+ "(function(){"
269
+ "var files=" +
270
+ filesJson +
271
+ ";"
272
+ // Global event — always fires
273
+ "window.dispatchEvent(new "
274
+ "CustomEvent('plusui:fileDrop.filesDropped',"
275
+ " { detail: { files: files } }));"
276
+ // Zone-specific delivery
277
+ "var el=document.elementFromPoint(" +
278
+ std::to_string(dpx) + "," + std::to_string(dpy) +
279
+ ");"
280
+ "var zone=el&&el.closest?el.closest('[data-dropzone]'):null;"
281
+ "var zoneName=zone?zone.getAttribute('data-dropzone'):null;"
282
+ "if(zoneName&&window.__plusui_fileDrop__){"
283
+ "window.__plusui_fileDrop__(zoneName,files);"
284
+ "}"
285
+ // If no zone matched but there is only one registered zone,
286
+ // deliver there anyway so a simple single-zone app always works.
287
+ "if(!zoneName&&window.__plusui_fileDrop__&&"
288
+ "window.__plusui_fileDrop_default__){"
289
+ "window.__plusui_fileDrop_default__(files);"
290
+ "}"
291
+ "})();";
292
+
293
+ targetImpl->webview->ExecuteScript(
294
+ std::wstring(eventScript.begin(), eventScript.end()).c_str(),
295
+ nullptr);
296
+ return 0;
299
297
  }
300
298
  case WM_SETFOCUS:
301
299
  impl->state.isFocused = true;
@@ -980,17 +978,17 @@ Window Window::create(void *windowHandle, const WindowConfig &config) {
980
978
  Window::Impl::embeddedWebviewByParent[parentHwnd] =
981
979
  pImpl.get();
982
980
 
983
- ComPtr<ICoreWebView2Controller4> controller4;
984
- if (controller &&
985
- SUCCEEDED(controller->QueryInterface(
986
- IID_PPV_ARGS(&controller4))) &&
987
- controller4) {
988
- bool allowExternalDrop =
989
- !(pImpl->config.enableFileDrop ||
990
- pImpl->config.disableWebviewDragDrop);
991
- controller4->put_AllowExternalDrop(
992
- allowExternalDrop ? TRUE : FALSE);
993
- }
981
+ // Always allow external drops at the WebView2 level
982
+ // so that WM_DROPFILES on the parent HWND fires.
983
+ // Browser-level drop behavior (navigating to the file)
984
+ // is blocked by the injected JS below instead.
985
+ ComPtr<ICoreWebView2Controller4> controller4;
986
+ if (controller &&
987
+ SUCCEEDED(controller->QueryInterface(
988
+ IID_PPV_ARGS(&controller4))) &&
989
+ controller4) {
990
+ controller4->put_AllowExternalDrop(TRUE);
991
+ }
994
992
 
995
993
  pImpl->nativeWebView = pImpl->webview.Get();
996
994
  pImpl->ready = true;
@@ -1020,10 +1018,11 @@ Window Window::create(void *windowHandle, const WindowConfig &config) {
1020
1018
  nullptr);
1021
1019
  }
1022
1020
 
1023
- // Disable webview drag & drop behavior if requested
1024
- // This prevents files from being loaded in the browser
1025
- // and allows the native FileDrop API to handle them
1026
- // instead
1021
+ // Block browser default drag-drop behavior (prevents
1022
+ // the browser from navigating to the dropped file) while
1023
+ // still allowing visual feedback on drop zones.
1024
+ // The actual file delivery happens via WM_DROPFILES
1025
+ // handled by the C++ WndProc, not by browser events.
1027
1026
  std::string dropzoneScript = R"(
1028
1027
  (function() {
1029
1028
  if (window.__plusui_dropzone_init) return;
@@ -1048,35 +1047,49 @@ Window Window::create(void *windowHandle, const WindowConfig &config) {
1048
1047
  if (activeZone) activeZone.classList.add('dropzone-active');
1049
1048
  };
1050
1049
 
1051
- var handleDrag = function(e) {
1050
+ // Always preventDefault to stop browser from navigating to file,
1051
+ // but show visual feedback when over a drop zone
1052
+ document.addEventListener('dragenter', function(e) {
1053
+ e.preventDefault();
1052
1054
  var zone = findDropZone(e);
1053
1055
  updateActiveZone(zone);
1054
1056
  if (e.dataTransfer) {
1055
1057
  try { e.dataTransfer.dropEffect = zone ? 'copy' : 'none'; } catch(_) {}
1056
1058
  }
1057
- if (!zone) {
1058
- e.preventDefault();
1059
- e.stopPropagation();
1059
+ }, true);
1060
+
1061
+ document.addEventListener('dragover', function(e) {
1062
+ e.preventDefault();
1063
+ var zone = findDropZone(e);
1064
+ updateActiveZone(zone);
1065
+ if (e.dataTransfer) {
1066
+ try { e.dataTransfer.dropEffect = zone ? 'copy' : 'none'; } catch(_) {}
1060
1067
  }
1061
- };
1068
+ }, true);
1062
1069
 
1063
- ['dragenter', 'dragover', 'dragleave'].forEach(function(ev) {
1064
- document.addEventListener(ev, handleDrag, true);
1065
- });
1070
+ document.addEventListener('dragleave', function(e) {
1071
+ e.preventDefault();
1072
+ var zone = findDropZone(e);
1073
+ updateActiveZone(zone);
1074
+ }, true);
1066
1075
 
1067
1076
  document.addEventListener('drop', function(e) {
1077
+ e.preventDefault();
1068
1078
  updateActiveZone(null);
1069
1079
  }, true);
1080
+
1081
+ // Also block at window level as a safety net
1082
+ window.addEventListener('dragover', function(e) { e.preventDefault(); }, true);
1083
+ window.addEventListener('drop', function(e) { e.preventDefault(); }, true);
1070
1084
  })();
1071
1085
  )";
1072
- pImpl->webview->AddScriptToExecuteOnDocumentCreated(
1073
- std::wstring(dropzoneScript.begin(),
1074
- dropzoneScript.end())
1075
- .c_str(),
1076
- nullptr);
1077
- }
1078
-
1079
- // Set up WebMessageReceived handler for JS->C++ bridge
1086
+ pImpl->webview->AddScriptToExecuteOnDocumentCreated(
1087
+ std::wstring(dropzoneScript.begin(),
1088
+ dropzoneScript.end())
1089
+ .c_str(),
1090
+ nullptr);
1091
+
1092
+ // Set up WebMessageReceived handler for JS->C++ bridge
1080
1093
  pImpl->webview->add_WebMessageReceived(
1081
1094
  Callback<
1082
1095
  ICoreWebView2WebMessageReceivedEventHandler>(
@@ -1495,21 +1508,19 @@ Window Window::create(void *windowHandle, const WindowConfig &config) {
1495
1508
  : FALSE);
1496
1509
  }
1497
1510
 
1498
- if (pImpl->controller) {
1499
- ComPtr<ICoreWebView2Controller4>
1500
- controller4;
1501
- if (SUCCEEDED(pImpl->controller.As(
1502
- &controller4)) &&
1503
- controller4) {
1504
- bool allowExternalDrop = !(
1505
- pImpl->config.enableFileDrop ||
1506
- pImpl->config
1507
- .disableWebviewDragDrop);
1508
- controller4->put_AllowExternalDrop(
1509
- allowExternalDrop ? TRUE
1510
- : FALSE);
1511
- }
1512
- }
1511
+ // Always keep external drops allowed
1512
+ // at the WebView2 level so WM_DROPFILES
1513
+ // fires on the parent HWND
1514
+ if (pImpl->controller) {
1515
+ ComPtr<ICoreWebView2Controller4>
1516
+ controller4;
1517
+ if (SUCCEEDED(pImpl->controller.As(
1518
+ &controller4)) &&
1519
+ controller4) {
1520
+ controller4->put_AllowExternalDrop(
1521
+ TRUE);
1522
+ }
1523
+ }
1513
1524
  }
1514
1525
  success = true;
1515
1526
  } else if (fileDropMethod == "isEnabled") {
@@ -1616,60 +1627,61 @@ Window Window::create(void *windowHandle, const WindowConfig &config) {
1616
1627
  [config.preferences setValue:@YES forKey:@"developerExtrasEnabled"];
1617
1628
  }
1618
1629
 
1619
- // Disable webview drag & drop behavior to allow native FileDrop API
1620
- if (win.pImpl->config.disableWebviewDragDrop) {
1621
- NSString *disableDragDropScript =
1622
- @"(function() {"
1623
- "if (window.__plusui_nativeFileDropOnly) return;"
1624
- "window.__plusui_nativeFileDropOnly = true;"
1625
- "var isOverDropZone = function(e) {"
1626
- "if (!e) return false;"
1627
- "var target = null;"
1628
- "if (typeof e.clientX === 'number' && typeof e.clientY === 'number' "
1629
- "&& document.elementFromPoint) { target = "
1630
- "document.elementFromPoint(e.clientX, e.clientY); }"
1631
- "if (!target && e.target && e.target.nodeType === 1) { target = "
1632
- "e.target; }"
1633
- "if (!target || !target.closest) return false;"
1634
- "return !!target.closest('[data-dropzone=\\\"true\\\"], "
1635
- ".filedrop-zone, .dropzone');"
1636
- "};"
1637
- "var zoneActive = false;"
1638
- "var emitZoneState = function(next) {"
1639
- "if (next === zoneActive) return;"
1640
- "zoneActive = next;"
1641
- "window.dispatchEvent(new CustomEvent(next ? "
1642
- "'plusui:fileDrop.dragEnter' : 'plusui:fileDrop.dragLeave', { detail: "
1643
- "{} }));"
1644
- "};"
1645
- "var block = function(e) {"
1646
- "if (!e) return false;"
1647
- "var overDropZone = isOverDropZone(e);"
1648
- "if (e.type === 'drop') { emitZoneState(false); } else { "
1649
- "emitZoneState(overDropZone); }"
1650
- "e.preventDefault();"
1651
- "e.stopPropagation();"
1652
- "if (typeof e.stopImmediatePropagation === 'function') "
1653
- "e.stopImmediatePropagation();"
1654
- "if (e.dataTransfer) { try { e.dataTransfer.dropEffect = overDropZone "
1655
- "? 'copy' : 'none'; } catch (_) {} }"
1656
- "return false;"
1657
- "};"
1658
- "window.__plusui_dragDropEvents = "
1659
- "['dragenter','dragover','dragleave','drop'];"
1660
- "window.__plusui_dragDropBlocker = block;"
1661
- "window.__plusui_dragDropEvents.forEach(function(eventName) {"
1662
- "window.addEventListener(eventName, block, true);"
1663
- "document.addEventListener(eventName, block, true);"
1664
- "});"
1665
- "})();";
1666
-
1667
- WKUserScript *userScript = [[WKUserScript alloc]
1668
- initWithSource:disableDragDropScript
1669
- injectionTime:WKUserScriptInjectionTimeAtDocumentEnd
1670
- forMainFrameOnly:NO];
1671
- [config.userContentController addUserScript:userScript];
1672
- }
1630
+ // Block browser default drag-drop while allowing drop zone visual feedback.
1631
+ // File delivery is handled natively by macOS drag APIs, not browser events.
1632
+ if (win.pImpl->config.disableWebviewDragDrop ||
1633
+ win.pImpl->config.enableFileDrop) {
1634
+ NSString *disableDragDropScript =
1635
+ @"(function() {"
1636
+ "if (window.__plusui_dropzone_init) return;"
1637
+ "window.__plusui_dropzone_init = true;"
1638
+ "var activeZone = null;"
1639
+ "var findDropZone = function(e) {"
1640
+ "var target = null;"
1641
+ "if (e && typeof e.clientX === 'number' && document.elementFromPoint) {"
1642
+ "target = document.elementFromPoint(e.clientX, e.clientY);"
1643
+ "}"
1644
+ "if (!target && e && e.target) target = e.target;"
1645
+ "if (!target || !target.closest) return null;"
1646
+ "return target.closest('[data-dropzone]');"
1647
+ "};"
1648
+ "var updateActiveZone = function(zone) {"
1649
+ "if (activeZone === zone) return;"
1650
+ "if (activeZone) activeZone.classList.remove('dropzone-active');"
1651
+ "activeZone = zone;"
1652
+ "if (activeZone) activeZone.classList.add('dropzone-active');"
1653
+ "};"
1654
+ "document.addEventListener('dragenter', function(e) {"
1655
+ "e.preventDefault();"
1656
+ "var zone = findDropZone(e);"
1657
+ "updateActiveZone(zone);"
1658
+ "if (e.dataTransfer) { try { e.dataTransfer.dropEffect = zone ? 'copy' : 'none'; } catch(_) {} }"
1659
+ "}, true);"
1660
+ "document.addEventListener('dragover', function(e) {"
1661
+ "e.preventDefault();"
1662
+ "var zone = findDropZone(e);"
1663
+ "updateActiveZone(zone);"
1664
+ "if (e.dataTransfer) { try { e.dataTransfer.dropEffect = zone ? 'copy' : 'none'; } catch(_) {} }"
1665
+ "}, true);"
1666
+ "document.addEventListener('dragleave', function(e) {"
1667
+ "e.preventDefault();"
1668
+ "var zone = findDropZone(e);"
1669
+ "updateActiveZone(zone);"
1670
+ "}, true);"
1671
+ "document.addEventListener('drop', function(e) {"
1672
+ "e.preventDefault();"
1673
+ "updateActiveZone(null);"
1674
+ "}, true);"
1675
+ "window.addEventListener('dragover', function(e) { e.preventDefault(); }, true);"
1676
+ "window.addEventListener('drop', function(e) { e.preventDefault(); }, true);"
1677
+ "})();";
1678
+
1679
+ WKUserScript *userScript = [[WKUserScript alloc]
1680
+ initWithSource:disableDragDropScript
1681
+ injectionTime:WKUserScriptInjectionTimeAtDocumentEnd
1682
+ forMainFrameOnly:NO];
1683
+ [config.userContentController addUserScript:userScript];
1684
+ }
1673
1685
 
1674
1686
  // Hide scrollbars if disabled
1675
1687
  if (!win.pImpl->config.scrollbars) {
@@ -2114,87 +2126,75 @@ void Window::injectCSS(const std::string &css) {
2114
2126
  executeScript(script);
2115
2127
  }
2116
2128
 
2117
- void Window::setWebviewDragDropEnabled(bool enabled) {
2118
- std::string script;
2119
-
2120
- if (enabled) {
2121
- // Enable webview drag-drop by removing our prevention handlers
2122
- script = R"(
2123
- (function() {
2124
- var events = window.__plusui_dragDropEvents || ['dragenter', 'dragover', 'dragleave', 'drop'];
2125
- var blocker = window.__plusui_dragDropBlocker;
2126
- if (blocker) {
2127
- events.forEach(function(eventName) {
2128
- window.removeEventListener(eventName, blocker, true);
2129
- document.removeEventListener(eventName, blocker, true);
2130
- });
2131
- }
2132
- delete window.__plusui_dragDropBlocker;
2133
- delete window.__plusui_dragDropEvents;
2134
- delete window.__plusui_dragDropDisabled;
2135
- delete window.__plusui_nativeFileDropOnly;
2136
- })();
2137
- )";
2138
- } else {
2139
- // Disable webview drag-drop by preventing default behavior
2140
- script = R"(
2141
- (function() {
2142
- if (window.__plusui_nativeFileDropOnly) return;
2143
- window.__plusui_nativeFileDropOnly = true;
2144
- window.__plusui_dragDropDisabled = true;
2145
-
2146
- var isOverDropZone = function(e) {
2147
- if (!e) return false;
2148
- var target = null;
2149
- if (typeof e.clientX === 'number' && typeof e.clientY === 'number' && document.elementFromPoint) {
2150
- target = document.elementFromPoint(e.clientX, e.clientY);
2151
- }
2152
- if (!target && e.target && e.target.nodeType === 1) {
2153
- target = e.target;
2154
- }
2155
- if (!target || !target.closest) return false;
2156
- return !!target.closest('[data-dropzone="true"], .filedrop-zone, .dropzone');
2157
- };
2158
-
2159
- var zoneActive = false;
2160
- var emitZoneState = function(next) {
2161
- if (next === zoneActive) return;
2162
- zoneActive = next;
2163
- window.dispatchEvent(new CustomEvent(next ? 'plusui:fileDrop.dragEnter' : 'plusui:fileDrop.dragLeave', { detail: {} }));
2164
- };
2165
-
2166
- var block = function(e) {
2167
- if (!e) return false;
2168
- var overDropZone = isOverDropZone(e);
2169
- if (e.type === 'drop') {
2170
- emitZoneState(false);
2171
- } else {
2172
- emitZoneState(overDropZone);
2173
- }
2174
- e.preventDefault();
2175
- e.stopPropagation();
2176
- if (typeof e.stopImmediatePropagation === 'function') {
2177
- e.stopImmediatePropagation();
2178
- }
2179
- if (e.dataTransfer) {
2180
- try { e.dataTransfer.dropEffect = overDropZone ? 'copy' : 'none'; } catch (_) {}
2181
- }
2182
- return false;
2183
- };
2184
-
2185
- window.__plusui_dragDropEvents = ['dragenter', 'dragover', 'dragleave', 'drop'];
2186
- window.__plusui_dragDropBlocker = block;
2187
-
2188
- window.__plusui_dragDropEvents.forEach(function(eventName) {
2189
- window.addEventListener(eventName, block, true);
2190
- document.addEventListener(eventName, block, true);
2191
- });
2192
- })();
2193
- )";
2194
- }
2195
-
2196
- executeScript(script);
2197
- }
2129
+ void Window::setWebviewDragDropEnabled(bool enabled) {
2130
+ if (enabled) {
2131
+ // Remove the dropzone init so it can be re-applied if needed later
2132
+ executeScript(R"(
2133
+ (function() {
2134
+ delete window.__plusui_dropzone_init;
2135
+ })();
2136
+ )");
2137
+ } else {
2138
+ // Install the same dropzone script used at init time
2139
+ executeScript(R"(
2140
+ (function() {
2141
+ if (window.__plusui_dropzone_init) return;
2142
+ window.__plusui_dropzone_init = true;
2143
+
2144
+ var activeZone = null;
2145
+
2146
+ var findDropZone = function(e) {
2147
+ var target = null;
2148
+ if (e && typeof e.clientX === 'number' && document.elementFromPoint) {
2149
+ target = document.elementFromPoint(e.clientX, e.clientY);
2150
+ }
2151
+ if (!target && e && e.target) target = e.target;
2152
+ if (!target || !target.closest) return null;
2153
+ return target.closest('[data-dropzone]');
2154
+ };
2155
+
2156
+ var updateActiveZone = function(zone) {
2157
+ if (activeZone === zone) return;
2158
+ if (activeZone) activeZone.classList.remove('dropzone-active');
2159
+ activeZone = zone;
2160
+ if (activeZone) activeZone.classList.add('dropzone-active');
2161
+ };
2162
+
2163
+ document.addEventListener('dragenter', function(e) {
2164
+ e.preventDefault();
2165
+ var zone = findDropZone(e);
2166
+ updateActiveZone(zone);
2167
+ if (e.dataTransfer) {
2168
+ try { e.dataTransfer.dropEffect = zone ? 'copy' : 'none'; } catch(_) {}
2169
+ }
2170
+ }, true);
2171
+
2172
+ document.addEventListener('dragover', function(e) {
2173
+ e.preventDefault();
2174
+ var zone = findDropZone(e);
2175
+ updateActiveZone(zone);
2176
+ if (e.dataTransfer) {
2177
+ try { e.dataTransfer.dropEffect = zone ? 'copy' : 'none'; } catch(_) {}
2178
+ }
2179
+ }, true);
2180
+
2181
+ document.addEventListener('dragleave', function(e) {
2182
+ e.preventDefault();
2183
+ var zone = findDropZone(e);
2184
+ updateActiveZone(zone);
2185
+ }, true);
2186
+
2187
+ document.addEventListener('drop', function(e) {
2188
+ e.preventDefault();
2189
+ updateActiveZone(null);
2190
+ }, true);
2191
+
2192
+ window.addEventListener('dragover', function(e) { e.preventDefault(); }, true);
2193
+ window.addEventListener('drop', function(e) { e.preventDefault(); }, true);
2194
+ })();
2195
+ )");
2196
+ }
2197
+ }
2198
2198
 
2199
2199
  void Window::onNavigationStart(NavigationCallback callback) {
2200
2200
  pImpl->navigationCallback = callback;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "plusui-native-core",
3
- "version": "0.1.99",
3
+ "version": "0.1.101",
4
4
  "description": "PlusUI Core framework (frontend + backend implementations)",
5
5
  "type": "module",
6
6
  "main": "./Core/Features/API/index.ts",