| | |
| | | class="matnr-print-element" |
| | | :class="{ |
| | | 'is-selected': interactive && selectedElementId === element.id, |
| | | 'is-hidden': element.visible === false |
| | | 'is-hidden': element.visible === false, |
| | | 'is-table': element.type === 'table' |
| | | }" |
| | | :style="getElementBoxStyle(element)" |
| | | @mousedown.stop="handleElementMouseDown($event, element)" |
| | |
| | | :style="getRectStyle(element)" |
| | | ></div> |
| | | |
| | | <table |
| | | <div |
| | | v-else-if="element.type === 'table'" |
| | | class="matnr-print-element__table" |
| | | :style="getTableStyle(element)" |
| | | > |
| | | <colgroup> |
| | | <col |
| | | v-for="(column, columnIndex) in element.columns" |
| | | :key="`${element.id}_col_${columnIndex}`" |
| | | :style="{ width: getUnitValue(column.width || 10) }" |
| | | /> |
| | | </colgroup> |
| | | <tbody> |
| | | <tr |
| | | v-for="(row, rowIndex) in element.rows" |
| | | :key="`${element.id}_row_${rowIndex}`" |
| | | :style="{ height: getUnitValue(row.height || 6) }" |
| | | <div |
| | | v-for="cell in element.resolvedCells" |
| | | :key="`${element.id}_${cell.row}_${cell.col}`" |
| | | class="matnr-print-element__table-cell" |
| | | :style="getTableCellStyle(cell, element)" |
| | | > |
| | | <div |
| | | class="matnr-print-element__table-cell-content" |
| | | :style="getTableCellContentStyle()" |
| | | > |
| | | <template |
| | | v-for="cell in getTableCellsForRow(element, rowIndex)" |
| | | :key="`${element.id}_${rowIndex}_${cell.col}`" |
| | | > |
| | | <td |
| | | :colspan="cell.colspan || 1" |
| | | :rowspan="cell.rowspan || 1" |
| | | :style="getTableCellStyle(cell, element)" |
| | | > |
| | | <div |
| | | class="matnr-print-element__table-cell-content" |
| | | :style="getTableCellContentStyle(cell, element)" |
| | | > |
| | | {{ cell.resolvedText }} |
| | | </div> |
| | | </td> |
| | | </template> |
| | | </tr> |
| | | </tbody> |
| | | </table> |
| | | {{ cell.resolvedText }} |
| | | </div> |
| | | </div> |
| | | </div> |
| | | |
| | | <template |
| | | v-if="interactive && selectedElementId === element.id && element.type === 'table'" |
| | | > |
| | | <span |
| | | v-for="handle in getTableColumnHandles(element)" |
| | | :key="`${element.id}_col_handle_${handle.index}`" |
| | | class="matnr-print-element__table-divider is-column" |
| | | :style="getTableDividerStyle(element, 'column', handle)" |
| | | @mousedown.stop="handleTableResizeMouseDown($event, element, 'column', handle.index)" |
| | | ></span> |
| | | <span |
| | | v-for="handle in getTableRowHandles(element)" |
| | | :key="`${element.id}_row_handle_${handle.index}`" |
| | | class="matnr-print-element__table-divider is-row" |
| | | :style="getTableDividerStyle(element, 'row', handle)" |
| | | @mousedown.stop="handleTableResizeMouseDown($event, element, 'row', handle.index)" |
| | | ></span> |
| | | </template> |
| | | |
| | | <template v-if="interactive && selectedElementId === element.id && element.type !== 'line'"> |
| | | <span |
| | |
| | | |
| | | const canvasRef = ref(null) |
| | | const interactionState = ref(null) |
| | | const TABLE_MIN_COLUMN_WIDTH = 6 |
| | | const TABLE_MIN_ROW_HEIGHT = 4 |
| | | const resizeHandles = [ |
| | | { direction: 'top-left' }, |
| | | { direction: 'top' }, |
| | |
| | | return { |
| | | fontSize: editorMode.value ? mmToPx(style.fontSize, props.scale) : `${style.fontSize}mm`, |
| | | color: style.color, |
| | | tableLayout: 'fixed' |
| | | position: 'relative' |
| | | } |
| | | } |
| | | |
| | | function getTableColumnSpanWidth(element, cell) { |
| | | const columns = Array.isArray(element?.columns) ? element.columns : [] |
| | | const startCol = Number(cell?.col) || 0 |
| | | const colspan = Math.max(1, Number(cell?.colspan) || 1) |
| | | return columns |
| | | .slice(startCol, startCol + colspan) |
| | | .reduce((total, column) => total + (Number(column?.width) || 6), 0) |
| | | } |
| | | |
| | | function getTableCellSpanHeight(element, cell) { |
| | |
| | | |
| | | function getTableCellStyle(cell, element) { |
| | | const style = cell.style || {} |
| | | const borderWidth = editorMode.value |
| | | ? mmToPx(style.borderWidth || 0.2, props.scale) |
| | | : `${style.borderWidth || 0.2}mm` |
| | | const borderColor = style.borderColor || '#111111' |
| | | const columns = Array.isArray(element?.columns) ? element.columns : [] |
| | | const rows = Array.isArray(element?.rows) ? element.rows : [] |
| | | const left = columns |
| | | .slice(0, Math.max(0, Number(cell?.col) || 0)) |
| | | .reduce((total, column) => total + (Number(column?.width) || 6), 0) |
| | | const top = rows |
| | | .slice(0, Math.max(0, Number(cell?.row) || 0)) |
| | | .reduce((total, row) => total + (Number(row?.height) || 6), 0) |
| | | |
| | | return { |
| | | border: `${editorMode.value ? mmToPx(style.borderWidth || 0.2, props.scale) : `${style.borderWidth || 0.2}mm`} solid ${style.borderColor || '#111111'}`, |
| | | position: 'absolute', |
| | | left: getUnitValue(left), |
| | | top: getUnitValue(top), |
| | | width: getUnitValue(getTableColumnSpanWidth(element, cell)), |
| | | height: getUnitValue(getTableCellSpanHeight(element, cell)), |
| | | borderTop: Number(cell?.row) === 0 ? `${borderWidth} solid ${borderColor}` : 'none', |
| | | borderLeft: Number(cell?.col) === 0 ? `${borderWidth} solid ${borderColor}` : 'none', |
| | | borderRight: `${borderWidth} solid ${borderColor}`, |
| | | borderBottom: `${borderWidth} solid ${borderColor}`, |
| | | backgroundColor: style.backgroundColor || '#FFFFFF', |
| | | textAlign: style.textAlign || 'left', |
| | | fontWeight: style.fontWeight || 400, |
| | | height: getUnitValue(getTableCellSpanHeight(element, cell)), |
| | | boxSizing: 'border-box', |
| | | padding: 0, |
| | | overflow: 'hidden' |
| | | } |
| | | } |
| | | |
| | | function getTableCellContentStyle(cell, element) { |
| | | function getTableCellContentStyle() { |
| | | const paddingY = editorMode.value ? `${0.4 * props.scale}px` : '0.4mm' |
| | | const paddingX = editorMode.value ? `${0.8 * props.scale}px` : '0.8mm' |
| | | return { |
| | | width: '100%', |
| | | height: getUnitValue(getTableCellSpanHeight(element, cell)), |
| | | height: '100%', |
| | | boxSizing: 'border-box', |
| | | padding: `${paddingY} ${paddingX}`, |
| | | overflow: 'hidden', |
| | |
| | | } |
| | | } |
| | | |
| | | function getTableCellsForRow(element, rowIndex) { |
| | | return (Array.isArray(element?.resolvedCells) ? element.resolvedCells : []).filter( |
| | | (cell) => Number(cell?.row) === rowIndex |
| | | ) |
| | | function getTableColumnHandles(element) { |
| | | const columns = Array.isArray(element?.columns) ? element.columns : [] |
| | | let offset = 0 |
| | | return columns.slice(0, -1).map((column, index) => { |
| | | offset += Number(column?.width) || 0 |
| | | return { |
| | | index, |
| | | offset |
| | | } |
| | | }) |
| | | } |
| | | |
| | | function getTableRowHandles(element) { |
| | | const rows = Array.isArray(element?.rows) ? element.rows : [] |
| | | let offset = 0 |
| | | return rows.slice(0, -1).map((row, index) => { |
| | | offset += Number(row?.height) || 0 |
| | | return { |
| | | index, |
| | | offset |
| | | } |
| | | }) |
| | | } |
| | | |
| | | function getTableDividerStyle(element, axis, handle) { |
| | | const baseOffset = Number(handle?.offset) || 0 |
| | | |
| | | return axis === 'column' |
| | | ? { left: getUnitValue(baseOffset) } |
| | | : { top: getUnitValue(baseOffset) } |
| | | } |
| | | |
| | | function getQrcodeSize(element) { |
| | |
| | | return |
| | | } |
| | | emit('select-element', element.id) |
| | | startInteraction(event, element, 'resize', direction) |
| | | startInteraction(event, element, 'resize', direction, { |
| | | tableColumns: |
| | | element.type === 'table' && Array.isArray(element?.columns) |
| | | ? element.columns.map((column) => ({ |
| | | width: Number(column?.width) || TABLE_MIN_COLUMN_WIDTH |
| | | })) |
| | | : [], |
| | | tableRows: |
| | | element.type === 'table' && Array.isArray(element?.rows) |
| | | ? element.rows.map((row) => ({ |
| | | height: Number(row?.height) || TABLE_MIN_ROW_HEIGHT |
| | | })) |
| | | : [], |
| | | elementType: element?.type || '' |
| | | }) |
| | | } |
| | | |
| | | function startInteraction(event, element, type, direction = '') { |
| | | function handleTableResizeMouseDown(event, element, axis, index) { |
| | | if (!props.interactive) { |
| | | return |
| | | } |
| | | emit('select-element', element.id) |
| | | startInteraction(event, element, `table-${axis}-resize`, '', { |
| | | index, |
| | | columns: Array.isArray(element?.columns) |
| | | ? element.columns.map((column) => ({ |
| | | width: Number(column?.width) || TABLE_MIN_COLUMN_WIDTH |
| | | })) |
| | | : [], |
| | | rows: Array.isArray(element?.rows) |
| | | ? element.rows.map((row) => ({ |
| | | height: Number(row?.height) || TABLE_MIN_ROW_HEIGHT |
| | | })) |
| | | : [] |
| | | }) |
| | | } |
| | | |
| | | function startInteraction(event, element, type, direction = '', extra = {}) { |
| | | interactionState.value = { |
| | | type, |
| | | direction, |
| | |
| | | y: Number(element.y) || 0, |
| | | w: Number(element.w) || 0, |
| | | h: Number(element.h) || 0 |
| | | } |
| | | }, |
| | | ...extra |
| | | } |
| | | window.addEventListener('mousemove', handleWindowMouseMove) |
| | | window.addEventListener('mouseup', stopInteraction) |
| | |
| | | y: Math.max(0, origin.y + dy) |
| | | } |
| | | } else if (interactionState.value.type === 'resize') { |
| | | patch = buildResizePatch(origin, dx, dy, interactionState.value.direction) |
| | | patch = |
| | | interactionState.value.elementType === 'table' |
| | | ? buildTableElementResizePatch( |
| | | origin, |
| | | dx, |
| | | dy, |
| | | interactionState.value.direction, |
| | | interactionState.value.tableColumns, |
| | | interactionState.value.tableRows |
| | | ) |
| | | : buildResizePatch(origin, dx, dy, interactionState.value.direction) |
| | | } else if (interactionState.value.type === 'table-column-resize') { |
| | | patch = buildTableColumnResizePatch( |
| | | interactionState.value.columns, |
| | | interactionState.value.index, |
| | | dx |
| | | ) |
| | | } else if (interactionState.value.type === 'table-row-resize') { |
| | | patch = buildTableRowResizePatch( |
| | | interactionState.value.rows, |
| | | interactionState.value.index, |
| | | dy |
| | | ) |
| | | } |
| | | |
| | | emit('update-element', { |
| | |
| | | } |
| | | |
| | | return next |
| | | } |
| | | |
| | | function buildTableElementResizePatch(origin, dx, dy, direction, columns, rows) { |
| | | const basePatch = buildResizePatch(origin, dx, dy, direction) |
| | | const nextColumns = scaleTableTracks(columns, 'width', basePatch.w, TABLE_MIN_COLUMN_WIDTH) |
| | | const nextRows = scaleTableTracks(rows, 'height', basePatch.h, TABLE_MIN_ROW_HEIGHT) |
| | | const nextWidth = Number( |
| | | nextColumns.reduce((total, column) => total + (Number(column?.width) || 0), 0).toFixed(2) |
| | | ) |
| | | const nextHeight = Number( |
| | | nextRows.reduce((total, row) => total + (Number(row?.height) || 0), 0).toFixed(2) |
| | | ) |
| | | |
| | | return { |
| | | ...basePatch, |
| | | x: direction.includes('left') ? Math.max(0, origin.x + origin.w - nextWidth) : basePatch.x, |
| | | y: direction.includes('top') ? Math.max(0, origin.y + origin.h - nextHeight) : basePatch.y, |
| | | w: nextWidth, |
| | | h: nextHeight, |
| | | columns: nextColumns, |
| | | rows: nextRows |
| | | } |
| | | } |
| | | |
| | | function scaleTableTracks(tracks, key, targetTotal, minValue) { |
| | | const source = Array.isArray(tracks) |
| | | ? tracks.map((track) => ({ |
| | | [key]: Number(track?.[key]) || minValue |
| | | })) |
| | | : [] |
| | | if (!source.length) { |
| | | return [] |
| | | } |
| | | |
| | | const minimumTotal = minValue * source.length |
| | | const effectiveTarget = Math.max(minimumTotal, Number(targetTotal) || minimumTotal) |
| | | const sourceTotal = source.reduce((total, track) => total + (Number(track?.[key]) || 0), 0) |
| | | |
| | | let nextTracks = |
| | | sourceTotal > 0 |
| | | ? source.map((track) => ({ |
| | | [key]: Math.max( |
| | | minValue, |
| | | Number((((Number(track?.[key]) || 0) / sourceTotal) * effectiveTarget).toFixed(2)) |
| | | ) |
| | | })) |
| | | : source.map(() => ({ |
| | | [key]: Number((effectiveTarget / source.length).toFixed(2)) |
| | | })) |
| | | |
| | | let remainder = Number( |
| | | ( |
| | | effectiveTarget - |
| | | nextTracks.reduce((total, track) => total + (Number(track?.[key]) || 0), 0) |
| | | ).toFixed(2) |
| | | ) |
| | | |
| | | if (Math.abs(remainder) < 0.01) { |
| | | return nextTracks |
| | | } |
| | | |
| | | if (remainder > 0) { |
| | | nextTracks[nextTracks.length - 1] = { |
| | | ...nextTracks[nextTracks.length - 1], |
| | | [key]: Number((nextTracks[nextTracks.length - 1][key] + remainder).toFixed(2)) |
| | | } |
| | | return nextTracks |
| | | } |
| | | |
| | | let remaining = Math.abs(remainder) |
| | | for (let index = nextTracks.length - 1; index >= 0 && remaining > 0.009; index -= 1) { |
| | | const currentValue = Number(nextTracks[index]?.[key]) || minValue |
| | | const adjustable = Number(Math.max(0, currentValue - minValue).toFixed(2)) |
| | | if (adjustable <= 0) { |
| | | continue |
| | | } |
| | | const deduction = Math.min(adjustable, remaining) |
| | | nextTracks[index] = { |
| | | ...nextTracks[index], |
| | | [key]: Number((currentValue - deduction).toFixed(2)) |
| | | } |
| | | remaining = Number((remaining - deduction).toFixed(2)) |
| | | } |
| | | |
| | | return nextTracks |
| | | } |
| | | |
| | | function buildTableColumnResizePatch(columns, index, delta) { |
| | | const nextColumns = Array.isArray(columns) |
| | | ? columns.map((column) => ({ |
| | | width: Number(column?.width) || TABLE_MIN_COLUMN_WIDTH |
| | | })) |
| | | : [] |
| | | if (!nextColumns[index] || !nextColumns[index + 1]) { |
| | | return {} |
| | | } |
| | | |
| | | const leftWidth = nextColumns[index].width |
| | | const rightWidth = nextColumns[index + 1].width |
| | | const boundedDelta = Math.min( |
| | | Math.max(delta, TABLE_MIN_COLUMN_WIDTH - leftWidth), |
| | | rightWidth - TABLE_MIN_COLUMN_WIDTH |
| | | ) |
| | | |
| | | nextColumns[index] = { |
| | | ...nextColumns[index], |
| | | width: Number((leftWidth + boundedDelta).toFixed(2)) |
| | | } |
| | | nextColumns[index + 1] = { |
| | | ...nextColumns[index + 1], |
| | | width: Number((rightWidth - boundedDelta).toFixed(2)) |
| | | } |
| | | |
| | | return { |
| | | columns: nextColumns, |
| | | w: Number( |
| | | nextColumns.reduce((total, column) => total + (Number(column?.width) || 0), 0).toFixed(2) |
| | | ) |
| | | } |
| | | } |
| | | |
| | | function buildTableRowResizePatch(rows, index, delta) { |
| | | const nextRows = Array.isArray(rows) |
| | | ? rows.map((row) => ({ |
| | | height: Number(row?.height) || TABLE_MIN_ROW_HEIGHT |
| | | })) |
| | | : [] |
| | | if (!nextRows[index]) { |
| | | return {} |
| | | } |
| | | |
| | | const currentHeight = nextRows[index].height |
| | | const boundedDelta = Math.max(delta, TABLE_MIN_ROW_HEIGHT - currentHeight) |
| | | |
| | | nextRows[index] = { |
| | | ...nextRows[index], |
| | | height: Number((currentHeight + boundedDelta).toFixed(2)) |
| | | } |
| | | |
| | | return { |
| | | rows: nextRows, |
| | | h: Number(nextRows.reduce((total, row) => total + (Number(row?.height) || 0), 0).toFixed(2)) |
| | | } |
| | | } |
| | | |
| | | function stopInteraction() { |
| | |
| | | box-sizing: border-box; |
| | | overflow: hidden; |
| | | user-select: none; |
| | | } |
| | | |
| | | .matnr-print-element.is-table { |
| | | overflow: visible; |
| | | } |
| | | |
| | | .matnr-print-element.is-selected { |
| | |
| | | } |
| | | |
| | | .matnr-print-element__table { |
| | | border-collapse: collapse; |
| | | position: relative; |
| | | overflow: hidden; |
| | | } |
| | | |
| | | .matnr-print-element__table td { |
| | | .matnr-print-element__table-cell { |
| | | box-sizing: border-box; |
| | | vertical-align: middle; |
| | | } |
| | |
| | | box-sizing: border-box; |
| | | } |
| | | |
| | | .matnr-print-element__table-divider { |
| | | position: absolute; |
| | | z-index: 2; |
| | | background: transparent; |
| | | color: rgba(37, 99, 235, 0.7); |
| | | transition: |
| | | color 0.15s ease, |
| | | opacity 0.15s ease; |
| | | opacity: 0.95; |
| | | } |
| | | |
| | | .matnr-print-element__table-divider::before { |
| | | position: absolute; |
| | | content: ''; |
| | | background: currentColor; |
| | | border-radius: 999px; |
| | | box-shadow: 0 0 0 1px rgba(255, 255, 255, 0.92); |
| | | } |
| | | |
| | | .matnr-print-element__table-divider.is-column { |
| | | top: 0; |
| | | bottom: 0; |
| | | width: 12px; |
| | | transform: translateX(-6px); |
| | | cursor: ew-resize; |
| | | } |
| | | |
| | | .matnr-print-element__table-divider.is-column::before { |
| | | top: calc(50% - 12px); |
| | | left: calc(50% - 2px); |
| | | width: 4px; |
| | | height: 24px; |
| | | } |
| | | |
| | | .matnr-print-element__table-divider.is-row { |
| | | left: 0; |
| | | right: 0; |
| | | height: 12px; |
| | | transform: translateY(-6px); |
| | | cursor: ns-resize; |
| | | } |
| | | |
| | | .matnr-print-element__table-divider.is-row::before { |
| | | top: calc(50% - 2px); |
| | | left: calc(50% - 12px); |
| | | width: 24px; |
| | | height: 4px; |
| | | } |
| | | |
| | | .matnr-print-element__table-divider:hover { |
| | | color: rgba(37, 99, 235, 1); |
| | | opacity: 1; |
| | | } |
| | | |
| | | .matnr-print-element__handle { |
| | | position: absolute; |
| | | width: 10px; |