feat(sub12a+): drop-position simulator in Simple Recipe Editor
Replaces the per-row 'highlight whole row' feedback (operator
couldn't tell whether the new step would land before or after the
hovered row) with a precise insertion-point indicator.
How it works:
- Each row's onDragOver computes ev.clientY vs row midpoint.
Above midpoint → insertion index = rowIndex (BEFORE).
Below midpoint → insertion index = rowIndex + 1 (AFTER).
- An <o_fp_drop_indicator> div lives BEFORE the first row and
AFTER every row. When state.dragOverIndex matches that slot's
index, the div expands from height:0 to a 2.25rem dashed-green
reservation strip with a ghost-preview chip ('↓ insert here →
<icon> <step name>').
- onDragStart captures the dragged step/template's name + icon
into state.dragPreviewLabel/Icon for the chip text.
- Smooth 80ms height/margin transition so the line glides between
slots as the cursor moves rather than blinking.
- Trailing dropzone retains its existing 'Drop here to add at end'
styling. Empty list shows 'Drag a library step here to start'.
- onDrop reads from state.dragOverIndex (set by the most-recent
onDragOver) so we drop at the simulated position exactly.
- onDragLeave guards against child-element flicker via
relatedTarget contains() check.
- onDragEnd clears state.dragPreviewLabel/Icon so a half-completed
drag (cancelled by Esc) doesn't leave the chip stuck on screen.
fusion_plating → 19.0.10.4.0.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -5,7 +5,7 @@
|
||||
|
||||
{
|
||||
'name': 'Fusion Plating',
|
||||
'version': '19.0.10.3.0',
|
||||
'version': '19.0.10.4.0',
|
||||
'category': 'Manufacturing/Plating',
|
||||
'summary': 'Core plating / metal finishing ERP: facilities, processes, tanks, baths, jobs, operators.',
|
||||
'description': """
|
||||
|
||||
@@ -32,7 +32,11 @@ export class FpSimpleRecipeEditor extends Component {
|
||||
librarySearch: "",
|
||||
templateOptions: [],
|
||||
selectedTemplate: "",
|
||||
dragOverIndex: null,
|
||||
// Drop-position simulator (snaps to line above/below the
|
||||
// hovered row based on cursor Y vs row midpoint).
|
||||
dragOverIndex: null, // 0..N (insertion index)
|
||||
dragPreviewLabel: "", // shown next to the indicator line
|
||||
dragPreviewIcon: "fa-cog",
|
||||
});
|
||||
|
||||
this._recipeId = null;
|
||||
@@ -155,25 +159,51 @@ export class FpSimpleRecipeEditor extends Component {
|
||||
ev.dataTransfer.effectAllowed = "move";
|
||||
ev.dataTransfer.setData("application/x-fp-step", String(stepId));
|
||||
ev.dataTransfer.setData("text/plain", String(stepId));
|
||||
const step = this.state.steps.find((s) => s.id === stepId);
|
||||
this.state.dragPreviewLabel = step ? step.name : "";
|
||||
this.state.dragPreviewIcon = (step && step.icon) || "fa-cog";
|
||||
}
|
||||
|
||||
onLibraryDragStart(templateId, ev) {
|
||||
ev.dataTransfer.effectAllowed = "copy";
|
||||
ev.dataTransfer.setData("application/x-fp-library", String(templateId));
|
||||
ev.dataTransfer.setData("text/plain", "library");
|
||||
const tpl = this.state.library.find((t) => t.id === templateId);
|
||||
this.state.dragPreviewLabel = tpl ? tpl.name : "";
|
||||
this.state.dragPreviewIcon = (tpl && tpl.icon) || "fa-cog";
|
||||
}
|
||||
|
||||
onDragOver(index, ev) {
|
||||
/**
|
||||
* Compute the insertion index from the cursor Y vs row midpoint:
|
||||
* above midpoint → insert BEFORE this row (index = rowIndex)
|
||||
* below midpoint → insert AFTER this row (index = rowIndex + 1)
|
||||
*/
|
||||
onRowDragOver(rowIndex, ev) {
|
||||
ev.preventDefault();
|
||||
ev.dataTransfer.dropEffect =
|
||||
ev.dataTransfer.types.includes("application/x-fp-library")
|
||||
? "copy"
|
||||
: "move";
|
||||
this.state.dragOverIndex = index;
|
||||
const rect = ev.currentTarget.getBoundingClientRect();
|
||||
const before = (ev.clientY - rect.top) < (rect.height / 2);
|
||||
this.state.dragOverIndex = before ? rowIndex : rowIndex + 1;
|
||||
}
|
||||
|
||||
async onDrop(targetIndex, ev) {
|
||||
/** Trailing dropzone — always inserts at the end. */
|
||||
onTailDragOver(ev) {
|
||||
ev.preventDefault();
|
||||
ev.dataTransfer.dropEffect =
|
||||
ev.dataTransfer.types.includes("application/x-fp-library")
|
||||
? "copy"
|
||||
: "move";
|
||||
this.state.dragOverIndex = this.state.steps.length;
|
||||
}
|
||||
|
||||
async onDrop(ev) {
|
||||
ev.preventDefault();
|
||||
const targetIndex = this.state.dragOverIndex !== null
|
||||
? this.state.dragOverIndex
|
||||
: this.state.steps.length;
|
||||
const fromLibrary = ev.dataTransfer.getData("application/x-fp-library");
|
||||
if (fromLibrary) {
|
||||
await this.insertFromLibrary(parseInt(fromLibrary, 10), targetIndex);
|
||||
@@ -184,11 +214,26 @@ export class FpSimpleRecipeEditor extends Component {
|
||||
await this.reorderStep(draggedId, targetIndex);
|
||||
}
|
||||
}
|
||||
this.state.dragOverIndex = null;
|
||||
this._clearDragState();
|
||||
}
|
||||
|
||||
onDragLeave() {
|
||||
onDragLeave(ev) {
|
||||
// Only clear when leaving the panel entirely. Browser fires
|
||||
// dragleave when crossing into a child element too — guard against
|
||||
// that by checking relatedTarget.
|
||||
if (!ev.currentTarget.contains(ev.relatedTarget)) {
|
||||
this.state.dragOverIndex = null;
|
||||
}
|
||||
}
|
||||
|
||||
onDragEnd() {
|
||||
this._clearDragState();
|
||||
}
|
||||
|
||||
_clearDragState() {
|
||||
this.state.dragOverIndex = null;
|
||||
this.state.dragPreviewLabel = "";
|
||||
this.state.dragPreviewIcon = "fa-cog";
|
||||
}
|
||||
|
||||
// --------------------------------------------------------------- helpers
|
||||
|
||||
@@ -93,6 +93,50 @@ $fp-se-drop: var(--fp-drop-bg, #{$_fp_se_drop_hex});
|
||||
}
|
||||
}
|
||||
|
||||
// ===================================================== Drop simulator
|
||||
//
|
||||
// Thin reservation line between rows that activates only when the
|
||||
// cursor crosses a row's vertical midpoint. The active indicator
|
||||
// expands to show a ghost-preview chip with the dragged step's icon
|
||||
// + name so the operator knows EXACTLY where the drop lands.
|
||||
|
||||
.o_fp_drop_indicator {
|
||||
height: 0;
|
||||
margin: 0;
|
||||
border: 0;
|
||||
transition: height .08s ease, margin .08s ease, background .08s;
|
||||
background: transparent;
|
||||
border-radius: 2px;
|
||||
overflow: hidden;
|
||||
|
||||
&.o_fp_drop_indicator_active {
|
||||
height: 2.25rem;
|
||||
margin: .25rem 0;
|
||||
background: $fp-se-drop;
|
||||
border: 2px dashed $fp-se-accent;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 .75rem;
|
||||
}
|
||||
|
||||
.o_fp_drop_label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: .5rem;
|
||||
font-weight: 600;
|
||||
color: $fp-se-accent;
|
||||
font-size: .85rem;
|
||||
|
||||
&::before {
|
||||
content: "↓ insert here →";
|
||||
font-weight: 500;
|
||||
color: $fp-se-muted;
|
||||
font-size: .75rem;
|
||||
margin-right: .5rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.o_fp_step_row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
@@ -33,17 +33,29 @@
|
||||
</div>
|
||||
|
||||
<div class="o_fp_simple_editor_body" t-if="!state.loading">
|
||||
<div class="o_fp_selected_panel">
|
||||
<div class="o_fp_selected_panel"
|
||||
t-on-dragleave="(ev) => this.onDragLeave(ev)"
|
||||
t-on-dragend="() => this.onDragEnd()"
|
||||
t-on-drop="(ev) => this.onDrop(ev)">
|
||||
<h3>Selected (drag to reorder)</h3>
|
||||
<div class="o_fp_steps_list">
|
||||
|
||||
<!-- Top drop indicator (insertion at index 0). Visible
|
||||
only when dragOverIndex === 0 — i.e. cursor is
|
||||
hovering above the first row's midpoint. -->
|
||||
<div class="o_fp_drop_indicator"
|
||||
t-att-class="state.dragOverIndex === 0 ? 'o_fp_drop_indicator_active' : ''">
|
||||
<span class="o_fp_drop_label" t-if="state.dragOverIndex === 0">
|
||||
<i t-att-class="'fa ' + (state.dragPreviewIcon || 'fa-cog')"/>
|
||||
<span t-esc="state.dragPreviewLabel"/>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<t t-foreach="state.steps" t-as="step" t-key="step.id">
|
||||
<div class="o_fp_step_row"
|
||||
t-att-class="state.dragOverIndex === step_index ? 'o_fp_drag_over' : ''"
|
||||
draggable="true"
|
||||
t-on-dragstart="(ev) => this.onSelectedDragStart(step.id, ev)"
|
||||
t-on-dragover="(ev) => this.onDragOver(step_index, ev)"
|
||||
t-on-dragleave="() => this.onDragLeave()"
|
||||
t-on-drop="(ev) => this.onDrop(step_index, ev)">
|
||||
t-on-dragover="(ev) => this.onRowDragOver(step_index, ev)">
|
||||
<span class="o_fp_drag_handle">⠿</span>
|
||||
<span class="o_fp_step_position"><t t-esc="step_index + 1"/>.</span>
|
||||
<i t-att-class="'fa ' + (step.icon || 'fa-cog')"/>
|
||||
@@ -57,13 +69,26 @@
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Indicator AFTER each row (insertion at index = step_index + 1) -->
|
||||
<div class="o_fp_drop_indicator"
|
||||
t-att-class="state.dragOverIndex === (step_index + 1) ? 'o_fp_drop_indicator_active' : ''">
|
||||
<span class="o_fp_drop_label" t-if="state.dragOverIndex === (step_index + 1)">
|
||||
<i t-att-class="'fa ' + (state.dragPreviewIcon || 'fa-cog')"/>
|
||||
<span t-esc="state.dragPreviewLabel"/>
|
||||
</span>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
<div class="o_fp_step_dropzone"
|
||||
t-att-class="state.dragOverIndex === state.steps.length ? 'o_fp_drag_over' : ''"
|
||||
t-on-dragover="(ev) => this.onDragOver(state.steps.length, ev)"
|
||||
t-on-dragleave="() => this.onDragLeave()"
|
||||
t-on-drop="(ev) => this.onDrop(state.steps.length, ev)">
|
||||
Drop here to add at end
|
||||
t-on-dragover="(ev) => this.onTailDragOver(ev)">
|
||||
<t t-if="state.steps.length === 0">
|
||||
Drag a library step here to start
|
||||
</t>
|
||||
<t t-else="">
|
||||
Drop here to add at end
|
||||
</t>
|
||||
</div>
|
||||
</div>
|
||||
<button class="btn btn-secondary o_fp_inline_add"
|
||||
|
||||
Reference in New Issue
Block a user