diff --git a/crates/engine/src/cmd/datamerge.rs b/crates/engine/src/cmd/datamerge.rs index e03da2d05..c57d507fb 100644 --- a/crates/engine/src/cmd/datamerge.rs +++ b/crates/engine/src/cmd/datamerge.rs @@ -485,7 +485,9 @@ fn add_placeholder(d: &mut Document, sel: &mut Selection, p: &Value, name: &str) } else if explicit_story || role_param == Some("text") { false } else if role_param == Some("qr") { - true + // A QR role with the insertion point in text is refused below rather than bound to a + // selected frame the caret is not in. + sel.text.is_none() } else if role_param == Some("image") { // An image role with the insertion point in text places an inline image there. sel.text.is_none() @@ -585,6 +587,11 @@ fn add_placeholder(d: &mut Document, sel: &mut Selection, p: &Value, name: &str) st.replace(at..at, &marker); } d.adjust_text_ranges(story, at..at, marker.len()); + // The caret moves past the field, as after typing, so the next field goes after it. + if let Some(t) = sel.text.as_mut().filter(|t| t.story == story && t.cell.is_none()) { + let shift = |pos: usize| if pos >= at { pos + marker.len() } else { pos }; + (t.anchor, t.focus) = (shift(t.anchor), shift(t.focus)); + } PlaceholderAnchor::Text { story, start: at, end: at.saturating_add(marker.len()) } } else { let text = d.story(story).map(|st| st.text.clone()).unwrap_or_default(); diff --git a/crates/engine/src/cmd/datamerge/tests.rs b/crates/engine/src/cmd/datamerge/tests.rs index 5f5c4a118..9180964c2 100644 --- a/crates/engine/src/cmd/datamerge/tests.rs +++ b/crates/engine/src/cmd/datamerge/tests.rs @@ -831,6 +831,44 @@ fn a_qr_field_at_the_insertion_point_is_refused() { assert!(s.documents()[0].doc.data_merge.placeholders.is_empty()); } +/// An explicit QR role at a text insertion point is refused too, rather than binding the QR code +/// to a selected frame the caret is not in. +#[test] +fn an_explicit_qr_role_at_the_insertion_point_is_refused() { + let mut s = Session::new(); + new_doc(&mut s, 1); + select_csv(&mut s, "Name,#Code\nAda,hello\n"); + let made = text_frame(&mut s, [36.0, 36.0, 300.0, 300.0], "Hi ", None); + let other = text_frame(&mut s, [36.0, 400.0, 300.0, 600.0], "", None); + let other_id = designcraft_doc::ItemId(other["id"].as_u64().unwrap()); + let sid = StoryId(made["story"].as_u64().unwrap()); + let doc = s.doc_mut().unwrap(); + doc.selection = Selection::text(TextSel { story: sid, anchor: 3, focus: 3, frame: None, cell: None }); + doc.selection.items = vec![other_id]; + let msg = err(&mut s, "data.placeholder.add", json!({"field": "Code", "role": "qr"})); + assert!(msg.contains("bind to a frame"), "{msg}"); + let doc = &s.documents()[0].doc; + assert!(doc.data_merge.placeholders.is_empty()); + assert_eq!(doc.item(other_id).unwrap().label, "", "the other frame is not bound"); +} + +/// Fields inserted one after another at the insertion point keep their order: the caret moves +/// past each marker, as after typing. +#[test] +fn fields_inserted_at_the_insertion_point_keep_their_order() { + let mut s = Session::new(); + new_doc(&mut s, 1); + select_csv(&mut s, "Name,City,@Photo\nAda,Paris,/x.png\n"); + let made = text_frame(&mut s, [36.0, 36.0, 300.0, 300.0], "Hi ", None); + exec(&mut s, "text.select", json!({"story": made["story"], "anchor": 3, "focus": 3})); + exec(&mut s, "data.placeholder.add", json!({"field": "Name", "role": "text"})); + exec(&mut s, "data.placeholder.add", json!({"field": "City", "role": "text"})); + exec(&mut s, "data.placeholder.add", json!({"field": "Photo"})); + assert_eq!(texts(&s), vec!["Hi <><><>".to_string()]); + let t = s.documents()[0].selection.text.unwrap(); + assert_eq!((t.anchor, t.focus), (28, 28)); +} + /// A typed `<>` marker for an image column places the image too, from an absolute path; /// an explicit text role still writes the path as text. #[test] diff --git a/docs/agents.md b/docs/agents.md index b2ec4c156..8058510e6 100644 --- a/docs/agents.md +++ b/docs/agents.md @@ -91,8 +91,9 @@ external PDF/X certification. marks text or a frame as a field (`role`: text, image, qr, hyperlink). An image field (a `@` column) inserted at a text insertion point, or typed there as `<>`, merges as an inline image at its natural size, scaled down to fit the frame. The merge writes only text into a table cell or a note: an image field inserted there is refused unless `role` -is `text`, and its `<>` marker, inserted or typed, merges as the path. A QR field (a `#` column) inserted at a -text insertion point is refused; bind it to a frame instead. An image +is `text`, and its `<>` marker, inserted or typed, merges as the path. A QR field (a `#` column) or `role: "qr"` +at a text insertion point is refused; bind it to a frame with `item` instead. A field inserted at the insertion point +puts the caret after its marker. An image path is absolute or relative to the data file's folder, then the document's; a path that starts with one separator (`\Photos\a.jpg`) is tried from the drive's root first. `data.options` sets records, tiling and image fitting; `data.preview {record}` / `data.preview.stop` show one record without saving it.