nti_receipt_builder 0.1.0
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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +21 -0
- data/CODE_OF_CONDUCT.md +10 -0
- data/LICENSE.txt +21 -0
- data/README.md +272 -0
- data/Rakefile +12 -0
- data/app/assets/javascripts/nti_receipt_builder/index.js +16 -0
- data/app/assets/javascripts/nti_receipt_builder/receipt_builder_controller.js +297 -0
- data/app/assets/javascripts/nti_receipt_builder/receipt_canvas_controller.js +279 -0
- data/app/assets/javascripts/nti_receipt_builder/receipt_draggable_controller.js +154 -0
- data/app/assets/javascripts/nti_receipt_builder/receipt_print_controller.js +24 -0
- data/app/assets/javascripts/nti_receipt_builder/receipt_properties_controller.js +297 -0
- data/app/assets/stylesheets/nti_receipt_builder.css +238 -0
- data/app/controllers/concerns/nti_receipt_builder/templates_controller.rb +170 -0
- data/app/helpers/nti_receipt_builder/render_helper.rb +21 -0
- data/app/models/nti_receipt_builder/template.rb +79 -0
- data/app/views/nti_receipt_builder/render/_order_lines_element.html.erb +36 -0
- data/app/views/nti_receipt_builder/render/_overflow_banner.html.erb +5 -0
- data/app/views/nti_receipt_builder/render/_paper.html.erb +12 -0
- data/app/views/nti_receipt_builder/render/_text_element.html.erb +1 -0
- data/app/views/nti_receipt_builder/render/_variable_element.html.erb +1 -0
- data/app/views/nti_receipt_builder/templates/_canvas.html.erb +33 -0
- data/app/views/nti_receipt_builder/templates/_elements_panel.html.erb +49 -0
- data/app/views/nti_receipt_builder/templates/_errors.html.erb +21 -0
- data/app/views/nti_receipt_builder/templates/_lock_version_field.html.erb +2 -0
- data/app/views/nti_receipt_builder/templates/_print_assets.html.erb +15 -0
- data/app/views/nti_receipt_builder/templates/_properties_panel.html.erb +8 -0
- data/app/views/nti_receipt_builder/templates/_settings_panel.html.erb +130 -0
- data/app/views/nti_receipt_builder/templates/create.turbo_stream.erb +3 -0
- data/app/views/nti_receipt_builder/templates/edit.html.erb +54 -0
- data/app/views/nti_receipt_builder/templates/new.html.erb +115 -0
- data/app/views/nti_receipt_builder/templates/preview.html.erb +24 -0
- data/app/views/nti_receipt_builder/templates/print.html.erb +31 -0
- data/app/views/nti_receipt_builder/templates/render_preview.turbo_stream.erb +26 -0
- data/app/views/nti_receipt_builder/templates/update.turbo_stream.erb +7 -0
- data/config/importmap.rb +4 -0
- data/lib/generators/nti_receipt_builder/install_generator.rb +49 -0
- data/lib/generators/nti_receipt_builder/templates/create_nti_receipt_templates.rb +28 -0
- data/lib/generators/nti_receipt_builder/templates/initializer.rb +16 -0
- data/lib/generators/nti_receipt_builder/templates/turbo_stream_tags.rb +36 -0
- data/lib/nti_receipt_builder/configuration.rb +59 -0
- data/lib/nti_receipt_builder/elements.rb +46 -0
- data/lib/nti_receipt_builder/engine.rb +32 -0
- data/lib/nti_receipt_builder/errors.rb +13 -0
- data/lib/nti_receipt_builder/layout_validator.rb +285 -0
- data/lib/nti_receipt_builder/presenters/collection.rb +126 -0
- data/lib/nti_receipt_builder/presenters/element.rb +68 -0
- data/lib/nti_receipt_builder/render_result.rb +18 -0
- data/lib/nti_receipt_builder/renderer.rb +78 -0
- data/lib/nti_receipt_builder/set_default.rb +24 -0
- data/lib/nti_receipt_builder/template_form.rb +60 -0
- data/lib/nti_receipt_builder/type_inference.rb +26 -0
- data/lib/nti_receipt_builder/variable_definition.rb +74 -0
- data/lib/nti_receipt_builder/variable_resolver.rb +55 -0
- data/lib/nti_receipt_builder/variables.rb +159 -0
- data/lib/nti_receipt_builder/version.rb +5 -0
- data/lib/nti_receipt_builder.rb +50 -0
- metadata +175 -0
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
import { Controller } from "@hotwired/stimulus"
|
|
2
|
+
|
|
3
|
+
const PX_PER_MM_AT_100 = 96 / 25.4
|
|
4
|
+
const MIN_ELEMENT_DIMENSION_MM = 1.0
|
|
5
|
+
// Keep in sync with NtiReceiptBuilder::Presenters::Collection.
|
|
6
|
+
const MM_PER_PT = 0.3528
|
|
7
|
+
const LINE_HEIGHT_FACTOR = 1.25
|
|
8
|
+
|
|
9
|
+
// Connects to data-controller="nrb-canvas"
|
|
10
|
+
export default class extends Controller {
|
|
11
|
+
static targets = ["viewport", "paper", "marginGuides", "elementsLayer", "zoomRange", "zoomLabel"]
|
|
12
|
+
|
|
13
|
+
static values = {
|
|
14
|
+
widthMm: Number,
|
|
15
|
+
heightMm: Number,
|
|
16
|
+
heightMode: String,
|
|
17
|
+
marginTopMm: Number,
|
|
18
|
+
marginRightMm: Number,
|
|
19
|
+
marginBottomMm: Number,
|
|
20
|
+
marginLeftMm: Number,
|
|
21
|
+
gridMm: Number,
|
|
22
|
+
zoom: { type: Number, default: 1 },
|
|
23
|
+
variableLabels: Object
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
connect() {
|
|
27
|
+
this.updatePaperSize()
|
|
28
|
+
this.updateMarginGuides()
|
|
29
|
+
this.applyZoom()
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
get pxPerMm() {
|
|
33
|
+
return PX_PER_MM_AT_100 * this.zoomValue
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
toPx(mm) {
|
|
37
|
+
return mm * this.pxPerMm
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
toMm(px) {
|
|
41
|
+
return px / this.pxPerMm
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
round(mm) {
|
|
45
|
+
return Math.round(mm * 100) / 100
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
snap(mm) {
|
|
49
|
+
const grid = this.gridMmValue || 1
|
|
50
|
+
return this.round(Math.round(mm / grid) * grid)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
clampPosition(xMm, yMm, widthMm, heightMm) {
|
|
54
|
+
const maxX = Math.max(0, this.widthMmValue - widthMm)
|
|
55
|
+
const clampedX = Math.min(Math.max(0, xMm), maxX)
|
|
56
|
+
|
|
57
|
+
let clampedY = Math.max(0, yMm)
|
|
58
|
+
if (this.heightModeValue !== "content") {
|
|
59
|
+
const maxY = Math.max(0, this.heightMmValue - heightMm)
|
|
60
|
+
clampedY = Math.min(clampedY, maxY)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return { xMm: this.round(clampedX), yMm: this.round(clampedY) }
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
clampSize(widthMm, heightMm, xMm, yMm) {
|
|
67
|
+
const maxWidth = Math.max(MIN_ELEMENT_DIMENSION_MM, this.widthMmValue - xMm)
|
|
68
|
+
const clampedWidth = Math.min(Math.max(MIN_ELEMENT_DIMENSION_MM, widthMm), maxWidth)
|
|
69
|
+
|
|
70
|
+
let clampedHeight = Math.max(MIN_ELEMENT_DIMENSION_MM, heightMm)
|
|
71
|
+
if (this.heightModeValue !== "content") {
|
|
72
|
+
const maxHeight = Math.max(MIN_ELEMENT_DIMENSION_MM, this.heightMmValue - yMm)
|
|
73
|
+
clampedHeight = Math.min(clampedHeight, maxHeight)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return { widthMm: this.round(clampedWidth), heightMm: this.round(clampedHeight) }
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
pointToMm(clientX, clientY) {
|
|
80
|
+
const rect = this.paperTarget.getBoundingClientRect()
|
|
81
|
+
const xMm = this.toMm(clientX - rect.left)
|
|
82
|
+
const yMm = this.toMm(clientY - rect.top)
|
|
83
|
+
|
|
84
|
+
return {
|
|
85
|
+
xMm: this.round(Math.max(0, Math.min(xMm, this.widthMmValue))),
|
|
86
|
+
yMm: this.round(Math.max(0, Math.min(yMm, this.heightMmValue)))
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
centerMm(widthMm, heightMm) {
|
|
91
|
+
return {
|
|
92
|
+
xMm: this.round(Math.max(0, (this.widthMmValue - widthMm) / 2)),
|
|
93
|
+
yMm: this.round(Math.max(0, (this.heightMmValue - heightMm) / 2))
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
paperClicked() {
|
|
98
|
+
this.dispatch("deselect", { prefix: "receipt-canvas", bubbles: true })
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
zoomChanged(event) {
|
|
102
|
+
this.zoomValue = parseFloat(event.target.value)
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
zoomValueChanged(value) {
|
|
106
|
+
if (this.hasZoomLabelTarget) this.zoomLabelTarget.textContent = `${Math.round(value * 100)}%`
|
|
107
|
+
this.applyZoom()
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
widthMmValueChanged() {
|
|
111
|
+
this.updatePaperSize()
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
heightMmValueChanged() {
|
|
115
|
+
this.updatePaperSize()
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
heightModeValueChanged() {
|
|
119
|
+
this.updatePaperSize()
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
marginTopMmValueChanged() {
|
|
123
|
+
this.updateMarginGuides()
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
marginRightMmValueChanged() {
|
|
127
|
+
this.updateMarginGuides()
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
marginBottomMmValueChanged() {
|
|
131
|
+
this.updateMarginGuides()
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
marginLeftMmValueChanged() {
|
|
135
|
+
this.updateMarginGuides()
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
applyZoom() {
|
|
139
|
+
if (!this.hasPaperTarget) return
|
|
140
|
+
this.paperTarget.style.zoom = String(this.zoomValue)
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
updatePaperSize() {
|
|
144
|
+
if (!this.hasPaperTarget) return
|
|
145
|
+
this.paperTarget.style.width = `${this.widthMmValue}mm`
|
|
146
|
+
this.paperTarget.style.height = this.heightModeValue === "content" ? "auto" : `${this.heightMmValue}mm`
|
|
147
|
+
this.paperTarget.style.minHeight = this.heightModeValue === "content" ? `${this.heightMmValue}mm` : ""
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
updateMarginGuides() {
|
|
151
|
+
if (!this.hasMarginGuidesTarget) return
|
|
152
|
+
this.marginGuidesTarget.style.top = `${this.marginTopMmValue}mm`
|
|
153
|
+
this.marginGuidesTarget.style.right = `${this.marginRightMmValue}mm`
|
|
154
|
+
this.marginGuidesTarget.style.bottom = `${this.marginBottomMmValue}mm`
|
|
155
|
+
this.marginGuidesTarget.style.left = `${this.marginLeftMmValue}mm`
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
renderElements(layout, selectedId) {
|
|
159
|
+
if (!this.hasElementsLayerTarget) return
|
|
160
|
+
|
|
161
|
+
this.elementsLayerTarget.innerHTML = ""
|
|
162
|
+
const sorted = [...layout].sort((a, b) => (a.z_index || 0) - (b.z_index || 0))
|
|
163
|
+
sorted.forEach((element) => {
|
|
164
|
+
this.elementsLayerTarget.appendChild(this.buildElementNode(element, element.id === selectedId))
|
|
165
|
+
})
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
buildElementNode(element, selected) {
|
|
169
|
+
const node = document.createElement("div")
|
|
170
|
+
node.setAttribute("data-controller", "nrb-draggable")
|
|
171
|
+
node.setAttribute("data-nrb-draggable-element-id-value", element.id)
|
|
172
|
+
node.setAttribute("data-nrb-draggable-x-mm-value", element.x_mm)
|
|
173
|
+
node.setAttribute("data-nrb-draggable-y-mm-value", element.y_mm)
|
|
174
|
+
node.setAttribute("data-nrb-draggable-width-mm-value", element.width_mm)
|
|
175
|
+
node.setAttribute("data-nrb-draggable-height-mm-value", element.height_mm)
|
|
176
|
+
node.setAttribute("data-nrb-draggable-nrb-canvas-outlet", `#${this.element.id}`)
|
|
177
|
+
node.setAttribute("data-action", "pointerdown->nrb-draggable#dragStart")
|
|
178
|
+
|
|
179
|
+
node.classList.add("nrb-receipt-element", `nrb-receipt-element--${element.type}`)
|
|
180
|
+
if (selected) node.classList.add("nrb-receipt-element--selected")
|
|
181
|
+
if (element.visible === false) node.classList.add("nrb-receipt-element--hidden")
|
|
182
|
+
|
|
183
|
+
node.style.left = `${element.x_mm}mm`
|
|
184
|
+
node.style.top = `${element.y_mm}mm`
|
|
185
|
+
node.style.width = `${element.width_mm}mm`
|
|
186
|
+
node.style.height = `${element.height_mm}mm`
|
|
187
|
+
node.style.zIndex = String(element.z_index || 0)
|
|
188
|
+
|
|
189
|
+
const body = document.createElement("div")
|
|
190
|
+
body.className = "nrb-receipt-element-body"
|
|
191
|
+
this.renderElementContent(body, element)
|
|
192
|
+
node.appendChild(body)
|
|
193
|
+
|
|
194
|
+
const handle = document.createElement("div")
|
|
195
|
+
handle.className = "nrb-receipt-resize-handle"
|
|
196
|
+
handle.setAttribute("data-nrb-draggable-target", "resizeHandle")
|
|
197
|
+
handle.setAttribute("data-action", "pointerdown->nrb-draggable#resizeStart")
|
|
198
|
+
node.appendChild(handle)
|
|
199
|
+
|
|
200
|
+
return node
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
renderElementContent(body, element) {
|
|
204
|
+
if (element.type === "variable") {
|
|
205
|
+
this.applyTextStyles(body, element.styles || {})
|
|
206
|
+
const label = this.variableLabelsValue[element.variable_key] || element.variable_key || "Variable"
|
|
207
|
+
body.textContent = `${element.prefix || ""}{${label}}${element.suffix || ""}`
|
|
208
|
+
} else if (element.type === "text") {
|
|
209
|
+
this.applyTextStyles(body, element.styles || {})
|
|
210
|
+
body.textContent = element.text || ""
|
|
211
|
+
} else {
|
|
212
|
+
this.renderOrderLinesPlaceholder(body, element)
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
applyTextStyles(body, styles) {
|
|
217
|
+
body.style.fontSize = `${styles.font_size_pt || 10}pt`
|
|
218
|
+
body.style.fontWeight = styles.font_weight || "normal"
|
|
219
|
+
body.style.textAlign = styles.text_align || "left"
|
|
220
|
+
body.style.lineHeight = styles.line_height || 1.2
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// Mirrors NtiReceiptBuilder::Presenters::Collection / _order_lines_element.html.erb —
|
|
224
|
+
// same table markup and same layout math, so the canvas placeholder matches
|
|
225
|
+
// what actually prints. Keep the constants below in sync with the presenter.
|
|
226
|
+
renderOrderLinesPlaceholder(body, element) {
|
|
227
|
+
const config = element.config || {}
|
|
228
|
+
const columns = config.columns || []
|
|
229
|
+
const fontSizePt = config.font_size_pt || 9
|
|
230
|
+
const rowSpacingMm = config.row_spacing_mm != null ? config.row_spacing_mm : 1.0
|
|
231
|
+
const totalWidthMm = columns.reduce((sum, column) => sum + (Number(column.width_mm) || 0), 0)
|
|
232
|
+
|
|
233
|
+
const table = document.createElement("table")
|
|
234
|
+
table.className = "nrb-receipt-order-lines-table"
|
|
235
|
+
table.style.fontSize = `${fontSizePt}pt`
|
|
236
|
+
table.style.lineHeight = `${fontSizePt * MM_PER_PT * LINE_HEIGHT_FACTOR}mm`
|
|
237
|
+
|
|
238
|
+
const colgroup = document.createElement("colgroup")
|
|
239
|
+
columns.forEach((column) => {
|
|
240
|
+
const col = document.createElement("col")
|
|
241
|
+
col.style.width = `${this.columnWidthPct(column, columns.length, totalWidthMm)}%`
|
|
242
|
+
colgroup.appendChild(col)
|
|
243
|
+
})
|
|
244
|
+
table.appendChild(colgroup)
|
|
245
|
+
|
|
246
|
+
if (config.show_header !== false) {
|
|
247
|
+
const thead = document.createElement("thead")
|
|
248
|
+
const header = document.createElement("tr")
|
|
249
|
+
header.className = "nrb-receipt-order-lines-header"
|
|
250
|
+
columns.forEach((column) => header.appendChild(this.buildCell(column, column.label, rowSpacingMm, "th")))
|
|
251
|
+
thead.appendChild(header)
|
|
252
|
+
table.appendChild(thead)
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
const tbody = document.createElement("tbody")
|
|
256
|
+
;[1, 2].forEach(() => {
|
|
257
|
+
const row = document.createElement("tr")
|
|
258
|
+
columns.forEach((column) => row.appendChild(this.buildCell(column, "—", rowSpacingMm)))
|
|
259
|
+
tbody.appendChild(row)
|
|
260
|
+
})
|
|
261
|
+
table.appendChild(tbody)
|
|
262
|
+
|
|
263
|
+
body.appendChild(table)
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
columnWidthPct(column, columnCount, totalWidthMm) {
|
|
267
|
+
if (totalWidthMm > 0) return ((Number(column.width_mm) || 0) / totalWidthMm) * 100
|
|
268
|
+
|
|
269
|
+
return columnCount > 0 ? 100 / columnCount : 100
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
buildCell(column, text, rowSpacingMm, tag = "td") {
|
|
273
|
+
const cell = document.createElement(tag)
|
|
274
|
+
cell.style.textAlign = column.align || "left"
|
|
275
|
+
cell.style.paddingBottom = `${rowSpacingMm}mm`
|
|
276
|
+
cell.textContent = text
|
|
277
|
+
return cell
|
|
278
|
+
}
|
|
279
|
+
}
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import { Controller } from "@hotwired/stimulus"
|
|
2
|
+
|
|
3
|
+
// Connects to data-controller="nrb-draggable"
|
|
4
|
+
export default class extends Controller {
|
|
5
|
+
static values = {
|
|
6
|
+
elementId: String,
|
|
7
|
+
xMm: Number,
|
|
8
|
+
yMm: Number,
|
|
9
|
+
widthMm: Number,
|
|
10
|
+
heightMm: Number
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
static targets = ["resizeHandle"]
|
|
14
|
+
static outlets = ["nrb-canvas"]
|
|
15
|
+
|
|
16
|
+
// A gesture interrupted by navigation never reaches dragEnd/resizeEnd, so its cleanup path
|
|
17
|
+
// never runs and pointer capture is never released. Tear both down here.
|
|
18
|
+
disconnect() {
|
|
19
|
+
this.teardownDrag()
|
|
20
|
+
this.teardownResize()
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
teardownDrag() {
|
|
24
|
+
if (!this.boundDragMove) return
|
|
25
|
+
|
|
26
|
+
this.element.removeEventListener("pointermove", this.boundDragMove)
|
|
27
|
+
this.element.removeEventListener("pointerup", this.boundDragEnd)
|
|
28
|
+
this.element.removeEventListener("pointercancel", this.boundDragEnd)
|
|
29
|
+
this.element.classList.remove("nrb-receipt-element-dragging")
|
|
30
|
+
this.boundDragMove = null
|
|
31
|
+
this.boundDragEnd = null
|
|
32
|
+
this.dragOrigin = null
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
teardownResize() {
|
|
36
|
+
if (!this.boundResizeMove) return
|
|
37
|
+
|
|
38
|
+
if (this.hasResizeHandleTarget) {
|
|
39
|
+
this.resizeHandleTarget.removeEventListener("pointermove", this.boundResizeMove)
|
|
40
|
+
this.resizeHandleTarget.removeEventListener("pointerup", this.boundResizeEnd)
|
|
41
|
+
this.resizeHandleTarget.removeEventListener("pointercancel", this.boundResizeEnd)
|
|
42
|
+
}
|
|
43
|
+
this.element.classList.remove("nrb-receipt-element-resizing")
|
|
44
|
+
this.boundResizeMove = null
|
|
45
|
+
this.boundResizeEnd = null
|
|
46
|
+
this.resizeOrigin = null
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
dragStart(event) {
|
|
50
|
+
if (event.button !== 0) return
|
|
51
|
+
event.preventDefault()
|
|
52
|
+
event.stopPropagation()
|
|
53
|
+
|
|
54
|
+
this.dragOrigin = { clientX: event.clientX, clientY: event.clientY, xMm: this.xMmValue, yMm: this.yMmValue }
|
|
55
|
+
this.element.setPointerCapture(event.pointerId)
|
|
56
|
+
this.boundDragMove = this.dragMove.bind(this)
|
|
57
|
+
this.boundDragEnd = this.dragEnd.bind(this)
|
|
58
|
+
this.element.addEventListener("pointermove", this.boundDragMove)
|
|
59
|
+
this.element.addEventListener("pointerup", this.boundDragEnd)
|
|
60
|
+
this.element.addEventListener("pointercancel", this.boundDragEnd)
|
|
61
|
+
this.element.classList.add("nrb-receipt-element-dragging")
|
|
62
|
+
this.dispatchSelected()
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
dragMove(event) {
|
|
66
|
+
if (!this.dragOrigin || !this.hasNrbCanvasOutlet) return
|
|
67
|
+
|
|
68
|
+
const canvas = this.nrbCanvasOutlet
|
|
69
|
+
const deltaXMm = canvas.toMm(event.clientX - this.dragOrigin.clientX)
|
|
70
|
+
const deltaYMm = canvas.toMm(event.clientY - this.dragOrigin.clientY)
|
|
71
|
+
const snappedX = canvas.snap(this.dragOrigin.xMm + deltaXMm)
|
|
72
|
+
const snappedY = canvas.snap(this.dragOrigin.yMm + deltaYMm)
|
|
73
|
+
const { xMm, yMm } = canvas.clampPosition(snappedX, snappedY, this.widthMmValue, this.heightMmValue)
|
|
74
|
+
|
|
75
|
+
this.xMmValue = xMm
|
|
76
|
+
this.yMmValue = yMm
|
|
77
|
+
this.applyPosition()
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
dragEnd(event) {
|
|
81
|
+
if (!this.dragOrigin) return
|
|
82
|
+
|
|
83
|
+
this.element.releasePointerCapture(event.pointerId)
|
|
84
|
+
const detail = { id: this.elementIdValue, xMm: this.xMmValue, yMm: this.yMmValue }
|
|
85
|
+
this.teardownDrag()
|
|
86
|
+
|
|
87
|
+
this.dispatch("moved", { prefix: "receipt-element", bubbles: true, detail })
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
resizeStart(event) {
|
|
91
|
+
if (event.button !== 0) return
|
|
92
|
+
event.preventDefault()
|
|
93
|
+
event.stopPropagation()
|
|
94
|
+
|
|
95
|
+
this.resizeOrigin = {
|
|
96
|
+
clientX: event.clientX,
|
|
97
|
+
clientY: event.clientY,
|
|
98
|
+
widthMm: this.widthMmValue,
|
|
99
|
+
heightMm: this.heightMmValue,
|
|
100
|
+
xMm: this.xMmValue,
|
|
101
|
+
yMm: this.yMmValue
|
|
102
|
+
}
|
|
103
|
+
this.resizeHandleTarget.setPointerCapture(event.pointerId)
|
|
104
|
+
this.boundResizeMove = this.resizeMove.bind(this)
|
|
105
|
+
this.boundResizeEnd = this.resizeEnd.bind(this)
|
|
106
|
+
this.resizeHandleTarget.addEventListener("pointermove", this.boundResizeMove)
|
|
107
|
+
this.resizeHandleTarget.addEventListener("pointerup", this.boundResizeEnd)
|
|
108
|
+
this.resizeHandleTarget.addEventListener("pointercancel", this.boundResizeEnd)
|
|
109
|
+
this.element.classList.add("nrb-receipt-element-resizing")
|
|
110
|
+
this.dispatchSelected()
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
resizeMove(event) {
|
|
114
|
+
if (!this.resizeOrigin || !this.hasNrbCanvasOutlet) return
|
|
115
|
+
|
|
116
|
+
const canvas = this.nrbCanvasOutlet
|
|
117
|
+
const deltaWidthMm = canvas.toMm(event.clientX - this.resizeOrigin.clientX)
|
|
118
|
+
const deltaHeightMm = canvas.toMm(event.clientY - this.resizeOrigin.clientY)
|
|
119
|
+
const rawWidth = canvas.snap(this.resizeOrigin.widthMm + deltaWidthMm)
|
|
120
|
+
const rawHeight = canvas.snap(this.resizeOrigin.heightMm + deltaHeightMm)
|
|
121
|
+
const { widthMm, heightMm } = canvas.clampSize(rawWidth, rawHeight, this.resizeOrigin.xMm, this.resizeOrigin.yMm)
|
|
122
|
+
|
|
123
|
+
this.widthMmValue = widthMm
|
|
124
|
+
this.heightMmValue = heightMm
|
|
125
|
+
this.applyPosition()
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
resizeEnd(event) {
|
|
129
|
+
if (!this.resizeOrigin) return
|
|
130
|
+
|
|
131
|
+
this.resizeHandleTarget.releasePointerCapture(event.pointerId)
|
|
132
|
+
const detail = {
|
|
133
|
+
id: this.elementIdValue,
|
|
134
|
+
xMm: this.xMmValue,
|
|
135
|
+
yMm: this.yMmValue,
|
|
136
|
+
widthMm: this.widthMmValue,
|
|
137
|
+
heightMm: this.heightMmValue
|
|
138
|
+
}
|
|
139
|
+
this.teardownResize()
|
|
140
|
+
|
|
141
|
+
this.dispatch("resized", { prefix: "receipt-element", bubbles: true, detail })
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
dispatchSelected() {
|
|
145
|
+
this.dispatch("selected", { prefix: "receipt-element", bubbles: true, detail: { id: this.elementIdValue } })
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
applyPosition() {
|
|
149
|
+
this.element.style.left = `${this.xMmValue}mm`
|
|
150
|
+
this.element.style.top = `${this.yMmValue}mm`
|
|
151
|
+
this.element.style.width = `${this.widthMmValue}mm`
|
|
152
|
+
this.element.style.height = `${this.heightMmValue}mm`
|
|
153
|
+
}
|
|
154
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { Controller } from "@hotwired/stimulus"
|
|
2
|
+
|
|
3
|
+
// Connects to data-controller="nrb-print"
|
|
4
|
+
export default class extends Controller {
|
|
5
|
+
static values = { auto: { type: Boolean, default: true } }
|
|
6
|
+
|
|
7
|
+
connect() {
|
|
8
|
+
if (!this.autoValue) return
|
|
9
|
+
|
|
10
|
+
// Cancelled on disconnect: an un-cancelled frame fires window.print() after the element is
|
|
11
|
+
// gone, which surfaces a print dialog over whatever the user navigated to.
|
|
12
|
+
this.printFrame = requestAnimationFrame(() => {
|
|
13
|
+
this.printFrame = null
|
|
14
|
+
window.print()
|
|
15
|
+
})
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
disconnect() {
|
|
19
|
+
if (!this.printFrame) return
|
|
20
|
+
|
|
21
|
+
cancelAnimationFrame(this.printFrame)
|
|
22
|
+
this.printFrame = null
|
|
23
|
+
}
|
|
24
|
+
}
|