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',
|
'name': 'Fusion Plating',
|
||||||
'version': '19.0.10.3.0',
|
'version': '19.0.10.4.0',
|
||||||
'category': 'Manufacturing/Plating',
|
'category': 'Manufacturing/Plating',
|
||||||
'summary': 'Core plating / metal finishing ERP: facilities, processes, tanks, baths, jobs, operators.',
|
'summary': 'Core plating / metal finishing ERP: facilities, processes, tanks, baths, jobs, operators.',
|
||||||
'description': """
|
'description': """
|
||||||
|
|||||||
@@ -32,7 +32,11 @@ export class FpSimpleRecipeEditor extends Component {
|
|||||||
librarySearch: "",
|
librarySearch: "",
|
||||||
templateOptions: [],
|
templateOptions: [],
|
||||||
selectedTemplate: "",
|
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;
|
this._recipeId = null;
|
||||||
@@ -155,25 +159,51 @@ export class FpSimpleRecipeEditor extends Component {
|
|||||||
ev.dataTransfer.effectAllowed = "move";
|
ev.dataTransfer.effectAllowed = "move";
|
||||||
ev.dataTransfer.setData("application/x-fp-step", String(stepId));
|
ev.dataTransfer.setData("application/x-fp-step", String(stepId));
|
||||||
ev.dataTransfer.setData("text/plain", 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) {
|
onLibraryDragStart(templateId, ev) {
|
||||||
ev.dataTransfer.effectAllowed = "copy";
|
ev.dataTransfer.effectAllowed = "copy";
|
||||||
ev.dataTransfer.setData("application/x-fp-library", String(templateId));
|
ev.dataTransfer.setData("application/x-fp-library", String(templateId));
|
||||||
ev.dataTransfer.setData("text/plain", "library");
|
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.preventDefault();
|
||||||
ev.dataTransfer.dropEffect =
|
ev.dataTransfer.dropEffect =
|
||||||
ev.dataTransfer.types.includes("application/x-fp-library")
|
ev.dataTransfer.types.includes("application/x-fp-library")
|
||||||
? "copy"
|
? "copy"
|
||||||
: "move";
|
: "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.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");
|
const fromLibrary = ev.dataTransfer.getData("application/x-fp-library");
|
||||||
if (fromLibrary) {
|
if (fromLibrary) {
|
||||||
await this.insertFromLibrary(parseInt(fromLibrary, 10), targetIndex);
|
await this.insertFromLibrary(parseInt(fromLibrary, 10), targetIndex);
|
||||||
@@ -184,11 +214,26 @@ export class FpSimpleRecipeEditor extends Component {
|
|||||||
await this.reorderStep(draggedId, targetIndex);
|
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.dragOverIndex = null;
|
||||||
|
this.state.dragPreviewLabel = "";
|
||||||
|
this.state.dragPreviewIcon = "fa-cog";
|
||||||
}
|
}
|
||||||
|
|
||||||
// --------------------------------------------------------------- helpers
|
// --------------------------------------------------------------- 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 {
|
.o_fp_step_row {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|||||||
@@ -33,17 +33,29 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="o_fp_simple_editor_body" t-if="!state.loading">
|
<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>
|
<h3>Selected (drag to reorder)</h3>
|
||||||
<div class="o_fp_steps_list">
|
<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">
|
<t t-foreach="state.steps" t-as="step" t-key="step.id">
|
||||||
<div class="o_fp_step_row"
|
<div class="o_fp_step_row"
|
||||||
t-att-class="state.dragOverIndex === step_index ? 'o_fp_drag_over' : ''"
|
|
||||||
draggable="true"
|
draggable="true"
|
||||||
t-on-dragstart="(ev) => this.onSelectedDragStart(step.id, ev)"
|
t-on-dragstart="(ev) => this.onSelectedDragStart(step.id, ev)"
|
||||||
t-on-dragover="(ev) => this.onDragOver(step_index, ev)"
|
t-on-dragover="(ev) => this.onRowDragOver(step_index, ev)">
|
||||||
t-on-dragleave="() => this.onDragLeave()"
|
|
||||||
t-on-drop="(ev) => this.onDrop(step_index, ev)">
|
|
||||||
<span class="o_fp_drag_handle">⠿</span>
|
<span class="o_fp_drag_handle">⠿</span>
|
||||||
<span class="o_fp_step_position"><t t-esc="step_index + 1"/>.</span>
|
<span class="o_fp_step_position"><t t-esc="step_index + 1"/>.</span>
|
||||||
<i t-att-class="'fa ' + (step.icon || 'fa-cog')"/>
|
<i t-att-class="'fa ' + (step.icon || 'fa-cog')"/>
|
||||||
@@ -57,13 +69,26 @@
|
|||||||
×
|
×
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</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>
|
</t>
|
||||||
|
|
||||||
<div class="o_fp_step_dropzone"
|
<div class="o_fp_step_dropzone"
|
||||||
t-att-class="state.dragOverIndex === state.steps.length ? 'o_fp_drag_over' : ''"
|
t-att-class="state.dragOverIndex === state.steps.length ? 'o_fp_drag_over' : ''"
|
||||||
t-on-dragover="(ev) => this.onDragOver(state.steps.length, ev)"
|
t-on-dragover="(ev) => this.onTailDragOver(ev)">
|
||||||
t-on-dragleave="() => this.onDragLeave()"
|
<t t-if="state.steps.length === 0">
|
||||||
t-on-drop="(ev) => this.onDrop(state.steps.length, ev)">
|
Drag a library step here to start
|
||||||
Drop here to add at end
|
</t>
|
||||||
|
<t t-else="">
|
||||||
|
Drop here to add at end
|
||||||
|
</t>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<button class="btn btn-secondary o_fp_inline_add"
|
<button class="btn btn-secondary o_fp_inline_add"
|
||||||
|
|||||||
Reference in New Issue
Block a user