mirror of
https://github.com/duhanbalci/dreport.git
synced 2026-07-01 18:39:16 +00:00
format
This commit is contained in:
@@ -94,9 +94,13 @@ if (savedSchema) {
|
||||
currentSchema.value = savedSchema
|
||||
}
|
||||
|
||||
watch(currentSchema, (val) => {
|
||||
watch(
|
||||
currentSchema,
|
||||
(val) => {
|
||||
localStorage.setItem(SCHEMA_STORAGE_KEY, JSON.stringify(val))
|
||||
}, { deep: true })
|
||||
},
|
||||
{ deep: true },
|
||||
)
|
||||
|
||||
// --- Sample Invoice Data ---
|
||||
|
||||
@@ -125,10 +129,38 @@ const sampleData: Record<string, unknown> = {
|
||||
telefon: '+90 216 444 0018',
|
||||
},
|
||||
kalemler: [
|
||||
{ siraNo: 1, adi: 'Web Uygulama Gelistirme', miktar: 1, birim: 'Adet', birimFiyat: 45000, tutar: 45000 },
|
||||
{ siraNo: 2, adi: 'Mobil Uygulama Gelistirme', miktar: 1, birim: 'Adet', birimFiyat: 35000, tutar: 35000 },
|
||||
{ siraNo: 3, adi: 'UI/UX Tasarim Hizmeti', miktar: 40, birim: 'Saat', birimFiyat: 750, tutar: 30000 },
|
||||
{ siraNo: 4, adi: 'Sunucu Bakim Sozlesmesi (Yillik)', miktar: 1, birim: 'Adet', birimFiyat: 12000, tutar: 12000 },
|
||||
{
|
||||
siraNo: 1,
|
||||
adi: 'Web Uygulama Gelistirme',
|
||||
miktar: 1,
|
||||
birim: 'Adet',
|
||||
birimFiyat: 45000,
|
||||
tutar: 45000,
|
||||
},
|
||||
{
|
||||
siraNo: 2,
|
||||
adi: 'Mobil Uygulama Gelistirme',
|
||||
miktar: 1,
|
||||
birim: 'Adet',
|
||||
birimFiyat: 35000,
|
||||
tutar: 35000,
|
||||
},
|
||||
{
|
||||
siraNo: 3,
|
||||
adi: 'UI/UX Tasarim Hizmeti',
|
||||
miktar: 40,
|
||||
birim: 'Saat',
|
||||
birimFiyat: 750,
|
||||
tutar: 30000,
|
||||
},
|
||||
{
|
||||
siraNo: 4,
|
||||
adi: 'Sunucu Bakim Sozlesmesi (Yillik)',
|
||||
miktar: 1,
|
||||
birim: 'Adet',
|
||||
birimFiyat: 12000,
|
||||
tutar: 12000,
|
||||
},
|
||||
{ siraNo: 5, adi: 'SSL Sertifikasi', miktar: 3, birim: 'Adet', birimFiyat: 500, tutar: 1500 },
|
||||
],
|
||||
toplamlar: {
|
||||
@@ -370,10 +402,30 @@ const defaultInvoiceTemplate: Template = {
|
||||
columns: [
|
||||
{ id: 'col_sira', field: 'siraNo', title: '#', width: sz.fixed(10), align: 'center' },
|
||||
{ id: 'col_adi', field: 'adi', title: 'Urun / Hizmet', width: sz.fr(), align: 'left' },
|
||||
{ id: 'col_miktar', field: 'miktar', title: 'Miktar', width: sz.fixed(18), align: 'right' },
|
||||
{
|
||||
id: 'col_miktar',
|
||||
field: 'miktar',
|
||||
title: 'Miktar',
|
||||
width: sz.fixed(18),
|
||||
align: 'right',
|
||||
},
|
||||
{ id: 'col_birim', field: 'birim', title: 'Birim', width: sz.fixed(18), align: 'center' },
|
||||
{ id: 'col_fiyat', field: 'birimFiyat', title: 'Birim Fiyat', width: sz.fixed(28), align: 'right', format: 'currency' as const },
|
||||
{ id: 'col_tutar', field: 'tutar', title: 'Tutar', width: sz.fixed(28), align: 'right', format: 'currency' as const },
|
||||
{
|
||||
id: 'col_fiyat',
|
||||
field: 'birimFiyat',
|
||||
title: 'Birim Fiyat',
|
||||
width: sz.fixed(28),
|
||||
align: 'right',
|
||||
format: 'currency' as const,
|
||||
},
|
||||
{
|
||||
id: 'col_tutar',
|
||||
field: 'tutar',
|
||||
title: 'Tutar',
|
||||
width: sz.fixed(28),
|
||||
align: 'right',
|
||||
format: 'currency' as const,
|
||||
},
|
||||
],
|
||||
style: {
|
||||
fontSize: 9,
|
||||
@@ -486,12 +538,16 @@ function loadFromLocalStorage(): Template | null {
|
||||
const template = ref<Template>(loadFromLocalStorage() ?? structuredClone(defaultInvoiceTemplate))
|
||||
|
||||
let saveTimeout: ReturnType<typeof setTimeout> | null = null
|
||||
watch(template, (val) => {
|
||||
watch(
|
||||
template,
|
||||
(val) => {
|
||||
if (saveTimeout) clearTimeout(saveTimeout)
|
||||
saveTimeout = setTimeout(() => {
|
||||
localStorage.setItem(STORAGE_KEY, JSON.stringify(val))
|
||||
}, 500)
|
||||
}, { deep: true })
|
||||
},
|
||||
{ deep: true },
|
||||
)
|
||||
|
||||
// --- Editor ref ---
|
||||
|
||||
@@ -626,36 +682,120 @@ function resetTemplate() {
|
||||
<h1>dreport</h1>
|
||||
<span class="app-header__subtitle">Belge Tasarim Araci</span>
|
||||
<div style="flex: 1"></div>
|
||||
<input ref="fileInputRef" type="file" accept=".json" style="display: none" @change="onImportFile" />
|
||||
<input ref="schemaFileInputRef" type="file" accept=".json" style="display: none" @change="onSchemaImportFile" />
|
||||
<input
|
||||
ref="fileInputRef"
|
||||
type="file"
|
||||
accept=".json"
|
||||
style="display: none"
|
||||
@change="onImportFile"
|
||||
/>
|
||||
<input
|
||||
ref="schemaFileInputRef"
|
||||
type="file"
|
||||
accept=".json"
|
||||
style="display: none"
|
||||
@change="onSchemaImportFile"
|
||||
/>
|
||||
|
||||
<!-- Template operations -->
|
||||
<button class="header-btn header-btn--secondary" @click="resetTemplate" title="Sifirla">
|
||||
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M2 8a6 6 0 0 1 10.2-4.3L14 2v4h-4l1.7-1.7A4.5 4.5 0 1 0 12.5 8" /><path d="M12.5 8a4.5 4.5 0 0 1-8.2 2.5" /></svg>
|
||||
<svg
|
||||
viewBox="0 0 16 16"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.5"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path d="M2 8a6 6 0 0 1 10.2-4.3L14 2v4h-4l1.7-1.7A4.5 4.5 0 1 0 12.5 8" />
|
||||
<path d="M12.5 8a4.5 4.5 0 0 1-8.2 2.5" />
|
||||
</svg>
|
||||
Sifirla
|
||||
</button>
|
||||
<button class="header-btn header-btn--secondary" @click="triggerImport" title="Sablon Yukle">
|
||||
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M8 10V2m0 0L5 5m3-3 3 3" /><path d="M2 10v2a2 2 0 0 0 2 2h8a2 2 0 0 0 2-2v-2" /></svg>
|
||||
<svg
|
||||
viewBox="0 0 16 16"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.5"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path d="M8 10V2m0 0L5 5m3-3 3 3" />
|
||||
<path d="M2 10v2a2 2 0 0 0 2 2h8a2 2 0 0 0 2-2v-2" />
|
||||
</svg>
|
||||
Yukle
|
||||
</button>
|
||||
<button class="header-btn header-btn--secondary" @click="exportTemplate" title="Sablon Kaydet">
|
||||
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M8 2v8m0 0 3-3m-3 3L5 7" /><path d="M2 10v2a2 2 0 0 0 2 2h8a2 2 0 0 0 2-2v-2" /></svg>
|
||||
<button
|
||||
class="header-btn header-btn--secondary"
|
||||
@click="exportTemplate"
|
||||
title="Sablon Kaydet"
|
||||
>
|
||||
<svg
|
||||
viewBox="0 0 16 16"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.5"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path d="M8 2v8m0 0 3-3m-3 3L5 7" />
|
||||
<path d="M2 10v2a2 2 0 0 0 2 2h8a2 2 0 0 0 2-2v-2" />
|
||||
</svg>
|
||||
Kaydet
|
||||
</button>
|
||||
<button class="header-btn header-btn--secondary" @click="exportBundle" title="Sablon + Schema Birlikte Kaydet">
|
||||
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="1" width="12" height="14" rx="1.5" /><path d="M5 4h6M5 7h6M5 10h4" /></svg>
|
||||
<button
|
||||
class="header-btn header-btn--secondary"
|
||||
@click="exportBundle"
|
||||
title="Sablon + Schema Birlikte Kaydet"
|
||||
>
|
||||
<svg
|
||||
viewBox="0 0 16 16"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.5"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<rect x="2" y="1" width="12" height="14" rx="1.5" />
|
||||
<path d="M5 4h6M5 7h6M5 10h4" />
|
||||
</svg>
|
||||
Paket
|
||||
</button>
|
||||
|
||||
<div class="header-divider"></div>
|
||||
|
||||
<!-- Schema operations -->
|
||||
<button class="header-btn header-btn--secondary" @click="triggerSchemaImport" title="Schema Yukle">
|
||||
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M8 10V2m0 0L5 5m3-3 3 3" /><path d="M2 10v2a2 2 0 0 0 2 2h8a2 2 0 0 0 2-2v-2" /></svg>
|
||||
<button
|
||||
class="header-btn header-btn--secondary"
|
||||
@click="triggerSchemaImport"
|
||||
title="Schema Yukle"
|
||||
>
|
||||
<svg
|
||||
viewBox="0 0 16 16"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.5"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path d="M8 10V2m0 0L5 5m3-3 3 3" />
|
||||
<path d="M2 10v2a2 2 0 0 0 2 2h8a2 2 0 0 0 2-2v-2" />
|
||||
</svg>
|
||||
Schema
|
||||
</button>
|
||||
<button class="header-btn header-btn--secondary" @click="exportSchema" title="Schema Kaydet">
|
||||
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M8 2v8m0 0 3-3m-3 3L5 7" /><path d="M2 10v2a2 2 0 0 0 2 2h8a2 2 0 0 0 2-2v-2" /></svg>
|
||||
<svg
|
||||
viewBox="0 0 16 16"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.5"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path d="M8 2v8m0 0 3-3m-3 3L5 7" />
|
||||
<path d="M2 10v2a2 2 0 0 0 2 2h8a2 2 0 0 0 2-2v-2" />
|
||||
</svg>
|
||||
Schema
|
||||
</button>
|
||||
|
||||
@@ -663,7 +803,17 @@ function resetTemplate() {
|
||||
|
||||
<!-- Output -->
|
||||
<button class="header-btn" :disabled="pdfLoading" @click="downloadPdf">
|
||||
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="1" width="10" height="14" rx="1.5" /><path d="M6 5h4M6 8h4M6 11h2" /></svg>
|
||||
<svg
|
||||
viewBox="0 0 16 16"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.5"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<rect x="3" y="1" width="10" height="14" rx="1.5" />
|
||||
<path d="M6 5h4M6 8h4M6 11h2" />
|
||||
</svg>
|
||||
{{ pdfLoading ? 'Hazirlaniyor...' : 'PDF Onizle' }}
|
||||
</button>
|
||||
</header>
|
||||
|
||||
@@ -90,7 +90,7 @@ function schemaToLanguageInfo(): DexprLanguageInfo {
|
||||
const tree = schemaStore.schemaTree
|
||||
for (const child of tree.children) {
|
||||
if (child.type === 'object') {
|
||||
const fields = child.children.map(f => ({
|
||||
const fields = child.children.map((f) => ({
|
||||
name: f.key,
|
||||
type: schemaToDexprType(f),
|
||||
}))
|
||||
@@ -112,7 +112,9 @@ function schemaToLanguageInfo(): DexprLanguageInfo {
|
||||
return info
|
||||
}
|
||||
|
||||
function schemaToDexprType(node: SchemaNode): 'String' | 'Number' | 'Boolean' | 'Object' | 'NumberList' | 'StringList' {
|
||||
function schemaToDexprType(
|
||||
node: SchemaNode,
|
||||
): 'String' | 'Number' | 'Boolean' | 'Object' | 'NumberList' | 'StringList' {
|
||||
switch (node.type) {
|
||||
case 'number':
|
||||
case 'integer':
|
||||
@@ -134,7 +136,7 @@ function createState(doc: string): EditorState {
|
||||
return EditorState.create({
|
||||
doc,
|
||||
extensions: [
|
||||
EditorView.updateListener.of(update => {
|
||||
EditorView.updateListener.of((update) => {
|
||||
if (update.docChanged) {
|
||||
const val = update.state.doc.toString()
|
||||
if (val !== props.modelValue) {
|
||||
@@ -207,7 +209,9 @@ onBeforeUnmount(() => {
|
||||
})
|
||||
|
||||
// Disaridan gelen deger degisikligi (undo/redo vs.)
|
||||
watch(() => props.modelValue, (newVal) => {
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
(newVal) => {
|
||||
if (!view) return
|
||||
const current = view.state.doc.toString()
|
||||
if (current !== newVal) {
|
||||
@@ -215,14 +219,19 @@ watch(() => props.modelValue, (newVal) => {
|
||||
changes: { from: 0, to: current.length, insert: newVal ?? '' },
|
||||
})
|
||||
}
|
||||
})
|
||||
},
|
||||
)
|
||||
|
||||
// Schema degisince editor'u yeniden olustur (autocomplete guncellenmeli)
|
||||
watch(langInfo, () => {
|
||||
watch(
|
||||
langInfo,
|
||||
() => {
|
||||
if (!view) return
|
||||
const doc = view.state.doc.toString()
|
||||
view.setState(createState(doc))
|
||||
}, { deep: true })
|
||||
},
|
||||
{ deep: true },
|
||||
)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
@@ -8,11 +8,14 @@ import LayoutRenderer from './LayoutRenderer.vue'
|
||||
import InteractionOverlay from './InteractionOverlay.vue'
|
||||
import RulerBar from './RulerBar.vue'
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
handleErrors?: boolean
|
||||
}>(), {
|
||||
}>(),
|
||||
{
|
||||
handleErrors: true,
|
||||
})
|
||||
},
|
||||
)
|
||||
|
||||
const templateStore = useTemplateStore()
|
||||
const editorStore = useEditorStore()
|
||||
@@ -26,7 +29,14 @@ const emit = defineEmits<{
|
||||
}>()
|
||||
|
||||
// Layout engine — template + data'yı worker'a gönderir, WASM ile layout hesaplar
|
||||
const { layout, layoutMap, error, computing: compiling, generateBarcode, dispose } = useLayoutEngine(template, mockData, layoutVersion)
|
||||
const {
|
||||
layout,
|
||||
layoutMap,
|
||||
error,
|
||||
computing: compiling,
|
||||
generateBarcode,
|
||||
dispose,
|
||||
} = useLayoutEngine(template, mockData, layoutVersion)
|
||||
|
||||
// LayoutRenderer'ın barcode üretmek için kullanabileceği fonksiyon
|
||||
provide('generateBarcode', generateBarcode)
|
||||
@@ -86,7 +96,7 @@ let resizeObserver: ResizeObserver | null = null
|
||||
|
||||
onMounted(() => {
|
||||
if (containerRef.value) {
|
||||
resizeObserver = new ResizeObserver(entries => {
|
||||
resizeObserver = new ResizeObserver((entries) => {
|
||||
const entry = entries[0]
|
||||
if (entry) containerWidth.value = entry.contentRect.width
|
||||
})
|
||||
@@ -132,10 +142,7 @@ function onWheel(e: WheelEvent) {
|
||||
} else {
|
||||
// İki parmak pan (touchpad) veya normal scroll
|
||||
e.preventDefault()
|
||||
editorStore.setPan(
|
||||
editorStore.panX - e.deltaX,
|
||||
editorStore.panY - e.deltaY,
|
||||
)
|
||||
editorStore.setPan(editorStore.panX - e.deltaX, editorStore.panY - e.deltaY)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -169,7 +176,16 @@ function applyZoom(delta: number, clientX: number, clientY: number) {
|
||||
}
|
||||
|
||||
function onKeyDown(e: KeyboardEvent) {
|
||||
if (e.code === 'Space' && !e.repeat && !(e.target instanceof HTMLInputElement || e.target instanceof HTMLSelectElement || e.target instanceof HTMLTextAreaElement || (e.target as HTMLElement)?.isContentEditable)) {
|
||||
if (
|
||||
e.code === 'Space' &&
|
||||
!e.repeat &&
|
||||
!(
|
||||
e.target instanceof HTMLInputElement ||
|
||||
e.target instanceof HTMLSelectElement ||
|
||||
e.target instanceof HTMLTextAreaElement ||
|
||||
(e.target as HTMLElement)?.isContentEditable
|
||||
)
|
||||
) {
|
||||
e.preventDefault()
|
||||
spaceHeld.value = true
|
||||
}
|
||||
@@ -225,9 +241,18 @@ function onPointerUp(e: PointerEvent) {
|
||||
@pointerup="onPointerUp"
|
||||
>
|
||||
<!-- Sayfalar -->
|
||||
<div ref="pageRef" class="editor-canvas__pages" :style="[pagesContainerStyle, panTransform ? { transform: panTransform } : {}]">
|
||||
<div
|
||||
ref="pageRef"
|
||||
class="editor-canvas__pages"
|
||||
:style="[pagesContainerStyle, panTransform ? { transform: panTransform } : {}]"
|
||||
>
|
||||
<LayoutRenderer :layout="layout" :scale="scale" />
|
||||
<InteractionOverlay :scale="scale" :layout-map="layoutMap" :page-count="layoutPages.length" :page-height-px="pageHeightPx" />
|
||||
<InteractionOverlay
|
||||
:scale="scale"
|
||||
:layout-map="layoutMap"
|
||||
:page-count="layoutPages.length"
|
||||
:page-height-px="pageHeightPx"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -235,12 +260,8 @@ function onPointerUp(e: PointerEvent) {
|
||||
<div v-if="props.handleErrors && error" class="editor-canvas__error">
|
||||
{{ error }}
|
||||
</div>
|
||||
<div v-if="compiling" class="editor-canvas__compiling">
|
||||
Derleniyor...
|
||||
</div>
|
||||
<div class="editor-canvas__zoom">
|
||||
%{{ editorStore.zoomPercent }}
|
||||
</div>
|
||||
<div v-if="compiling" class="editor-canvas__compiling">Derleniyor...</div>
|
||||
<div class="editor-canvas__zoom">%{{ editorStore.zoomPercent }}</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -59,7 +59,12 @@ const layoutStyle = computed(() => {
|
||||
}
|
||||
|
||||
// justify (main-axis)
|
||||
const justifyMap = { start: 'flex-start', center: 'center', end: 'flex-end', 'space-between': 'space-between' }
|
||||
const justifyMap = {
|
||||
start: 'flex-start',
|
||||
center: 'center',
|
||||
end: 'flex-end',
|
||||
'space-between': 'space-between',
|
||||
}
|
||||
style.justifyContent = justifyMap[c.justify] || 'flex-start'
|
||||
}
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -19,7 +19,8 @@ const props = defineProps<{
|
||||
|
||||
const templateStore = useTemplateStore()
|
||||
const editorStore = useEditorStore()
|
||||
const { activeGuides, collectEdges, calculateSnap, calculateResizeSnap, clearGuides } = useSnapGuides()
|
||||
const { activeGuides, collectEdges, calculateSnap, calculateResizeSnap, clearGuides } =
|
||||
useSnapGuides()
|
||||
|
||||
// Tüm elemanları flat olarak topla (root hariç)
|
||||
const flatElements = computed(() => {
|
||||
@@ -69,7 +70,7 @@ const allContainers = computed(() => {
|
||||
/** Sayfa index'ine göre y offset hesapla (sayfalar arası gap dahil) */
|
||||
function pageYOffset(pageIndex: number): number {
|
||||
if (pageIndex <= 0) return 0
|
||||
const pageH = props.pageHeightPx ?? (templateStore.template.page.height * props.scale)
|
||||
const pageH = props.pageHeightPx ?? templateStore.template.page.height * props.scale
|
||||
return pageIndex * (pageH + PAGE_GAP_PX)
|
||||
}
|
||||
|
||||
@@ -118,7 +119,11 @@ const dropVisualIndex = ref<number | null>(null)
|
||||
const dropLogicalIndex = ref<number | null>(null)
|
||||
|
||||
/** Mouse pozisyonuna göre en derin container'ı bul */
|
||||
function findDeepestContainer(mouseX: number, mouseY: number, excludeId?: string): ContainerElement {
|
||||
function findDeepestContainer(
|
||||
mouseX: number,
|
||||
mouseY: number,
|
||||
excludeId?: string,
|
||||
): ContainerElement {
|
||||
const s = props.scale
|
||||
let best: ContainerElement = templateStore.template.root
|
||||
|
||||
@@ -135,7 +140,7 @@ function findDeepestContainer(mouseX: number, mouseY: number, excludeId?: string
|
||||
if (mouseX >= cx && mouseX <= cx + cw && mouseY >= cy && mouseY <= cy + ch) {
|
||||
// Daha küçük (daha derin) container'ı tercih et
|
||||
const bestL = props.layoutMap[best.id]
|
||||
if (!bestL || (cw * ch < bestL.width_mm * s * bestL.height_mm * s)) {
|
||||
if (!bestL || cw * ch < bestL.width_mm * s * bestL.height_mm * s) {
|
||||
best = c
|
||||
}
|
||||
}
|
||||
@@ -144,9 +149,16 @@ function findDeepestContainer(mouseX: number, mouseY: number, excludeId?: string
|
||||
}
|
||||
|
||||
/** Container içinde drop index hesapla */
|
||||
function computeDropIndex(container: ContainerElement, mouseX: number, mouseY: number, excludeId?: string) {
|
||||
function computeDropIndex(
|
||||
container: ContainerElement,
|
||||
mouseX: number,
|
||||
mouseY: number,
|
||||
excludeId?: string,
|
||||
) {
|
||||
const s = props.scale
|
||||
const flowChildren = container.children.filter(c => c.type !== 'page_break' && c.position.type !== 'absolute' && c.id !== excludeId)
|
||||
const flowChildren = container.children.filter(
|
||||
(c) => c.type !== 'page_break' && c.position.type !== 'absolute' && c.id !== excludeId,
|
||||
)
|
||||
const isRow = container.direction === 'row'
|
||||
|
||||
let visualIdx = flowChildren.length
|
||||
@@ -156,18 +168,26 @@ function computeDropIndex(container: ContainerElement, mouseX: number, mouseY: n
|
||||
if (!l) continue
|
||||
if (isRow) {
|
||||
const centerX = l.x_mm * s + (l.width_mm * s) / 2
|
||||
if (mouseX < centerX) { visualIdx = i; break }
|
||||
if (mouseX < centerX) {
|
||||
visualIdx = i
|
||||
break
|
||||
}
|
||||
} else {
|
||||
const centerY = l.y_mm * s + pageYOffset(l.pageIndex) + (l.height_mm * s) / 2
|
||||
if (mouseY < centerY) { visualIdx = i; break }
|
||||
if (mouseY < centerY) {
|
||||
visualIdx = i
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Mantıksal index: excludeId aynı container'daysa offset hesapla
|
||||
let logicalIdx = visualIdx
|
||||
if (excludeId) {
|
||||
const allFlow = container.children.filter(c => c.type !== 'page_break' && c.position.type !== 'absolute')
|
||||
const currentIdx = allFlow.findIndex(c => c.id === excludeId)
|
||||
const allFlow = container.children.filter(
|
||||
(c) => c.type !== 'page_break' && c.position.type !== 'absolute',
|
||||
)
|
||||
const currentIdx = allFlow.findIndex((c) => c.id === excludeId)
|
||||
if (currentIdx >= 0) {
|
||||
// visualIdx, excludeId çıkarılmış listede. Gerçek listedeki pozisyona çevir.
|
||||
// flowChildren zaten excludeId hariç, dolayısıyla visualIdx doğrudan gerçek insert indexi
|
||||
@@ -177,7 +197,10 @@ function computeDropIndex(container: ContainerElement, mouseX: number, mouseY: n
|
||||
let count = 0
|
||||
for (let i = 0; i < allFlow.length; i++) {
|
||||
if (allFlow[i].id === excludeId) continue
|
||||
if (count === visualIdx) { realIdx = i; break }
|
||||
if (count === visualIdx) {
|
||||
realIdx = i
|
||||
break
|
||||
}
|
||||
count++
|
||||
realIdx = i + 1
|
||||
}
|
||||
@@ -219,7 +242,9 @@ const dropIndicatorStyle = computed(() => {
|
||||
|
||||
// Sürüklenen elemanı çıkar
|
||||
const dragId = dragElementId.value
|
||||
const flowChildren = container.children.filter(c => c.type !== 'page_break' && c.position.type !== 'absolute' && c.id !== dragId)
|
||||
const flowChildren = container.children.filter(
|
||||
(c) => c.type !== 'page_break' && c.position.type !== 'absolute' && c.id !== dragId,
|
||||
)
|
||||
|
||||
const cl = props.layoutMap[container.id]
|
||||
if (!cl) return { display: 'none' }
|
||||
@@ -380,13 +405,18 @@ function onDragEnd() {
|
||||
window.removeEventListener('pointermove', onDragMove)
|
||||
window.removeEventListener('pointerup', onDragEnd)
|
||||
|
||||
if (isDragging.value && dragElementId.value && dropTargetContainerId.value !== null && dropLogicalIndex.value !== null) {
|
||||
if (
|
||||
isDragging.value &&
|
||||
dragElementId.value &&
|
||||
dropTargetContainerId.value !== null &&
|
||||
dropLogicalIndex.value !== null
|
||||
) {
|
||||
const currentParent = templateStore.getParent(dragElementId.value)
|
||||
const targetContainerId = dropTargetContainerId.value
|
||||
|
||||
if (currentParent && currentParent.id === targetContainerId) {
|
||||
// Aynı container içinde reorder
|
||||
const currentIdx = currentParent.children.findIndex(c => c.id === dragElementId.value)
|
||||
const currentIdx = currentParent.children.findIndex((c) => c.id === dragElementId.value)
|
||||
if (currentIdx !== -1 && currentIdx !== dropLogicalIndex.value) {
|
||||
templateStore.reorderChild(currentParent.id, currentIdx, dropLogicalIndex.value)
|
||||
}
|
||||
@@ -400,7 +430,9 @@ function onDragEnd() {
|
||||
dragElementId.value = null
|
||||
editorStore.setDragging(false)
|
||||
clearDropTarget()
|
||||
setTimeout(() => { didDrag.value = false }, 50)
|
||||
setTimeout(() => {
|
||||
didDrag.value = false
|
||||
}, 50)
|
||||
}
|
||||
|
||||
// --- Absolute eleman drag ---
|
||||
@@ -420,7 +452,12 @@ function onAbsoluteDragStart(e: PointerEvent, el: TemplateElement) {
|
||||
elY: el.position.y,
|
||||
}
|
||||
|
||||
collectEdges(props.layoutMap, el.id, templateStore.template.page.width, templateStore.template.page.height)
|
||||
collectEdges(
|
||||
props.layoutMap,
|
||||
el.id,
|
||||
templateStore.template.page.width,
|
||||
templateStore.template.page.height,
|
||||
)
|
||||
|
||||
window.addEventListener('pointermove', onAbsoluteDragMove)
|
||||
window.addEventListener('pointerup', onAbsoluteDragEnd)
|
||||
@@ -466,7 +503,9 @@ function onAbsoluteDragEnd() {
|
||||
absoluteDragId.value = null
|
||||
editorStore.setDragging(false)
|
||||
clearGuides()
|
||||
setTimeout(() => { didDrag.value = false }, 50)
|
||||
setTimeout(() => {
|
||||
didDrag.value = false
|
||||
}, 50)
|
||||
}
|
||||
|
||||
// --- Resize ---
|
||||
@@ -493,18 +532,34 @@ function onResizeStart(e: PointerEvent, elId: string, handle: string) {
|
||||
const s = props.scale
|
||||
|
||||
// Barkod ve görsel elemanları için aspect ratio'yu kaydet
|
||||
const el = flatElements.value.find(e => e.id === elId)
|
||||
resizeAspectRatio.value = ((el?.type === 'barcode' || el?.type === 'image') && l.height_mm > 0) ? l.width_mm / l.height_mm : 0
|
||||
const el = flatElements.value.find((e) => e.id === elId)
|
||||
resizeAspectRatio.value =
|
||||
(el?.type === 'barcode' || el?.type === 'image') && l.height_mm > 0
|
||||
? l.width_mm / l.height_mm
|
||||
: 0
|
||||
|
||||
resizeStart.value = {
|
||||
mouseX: e.clientX, mouseY: e.clientY,
|
||||
x: l.x_mm * s, y: l.y_mm * s,
|
||||
width: l.width_mm * s, height: l.height_mm * s,
|
||||
mouseX: e.clientX,
|
||||
mouseY: e.clientY,
|
||||
x: l.x_mm * s,
|
||||
y: l.y_mm * s,
|
||||
width: l.width_mm * s,
|
||||
height: l.height_mm * s,
|
||||
}
|
||||
resizeGhost.value = {
|
||||
x: l.x_mm * s,
|
||||
y: l.y_mm * s,
|
||||
width: l.width_mm * s,
|
||||
height: l.height_mm * s,
|
||||
}
|
||||
resizeGhost.value = { x: l.x_mm * s, y: l.y_mm * s, width: l.width_mm * s, height: l.height_mm * s }
|
||||
resizeFinalMm.value = { width: l.width_mm, height: l.height_mm }
|
||||
|
||||
collectEdges(props.layoutMap, elId, templateStore.template.page.width, templateStore.template.page.height)
|
||||
collectEdges(
|
||||
props.layoutMap,
|
||||
elId,
|
||||
templateStore.template.page.width,
|
||||
templateStore.template.page.height,
|
||||
)
|
||||
|
||||
window.addEventListener('pointermove', onResizeMove)
|
||||
window.addEventListener('pointerup', onResizeEnd)
|
||||
@@ -519,13 +574,21 @@ function onResizeMove(e: PointerEvent) {
|
||||
const pxToMm = 1 / props.scale
|
||||
const ar = resizeAspectRatio.value
|
||||
|
||||
let gx = resizeStart.value.x, gy = resizeStart.value.y
|
||||
let gw = resizeStart.value.width, gh = resizeStart.value.height
|
||||
let gx = resizeStart.value.x,
|
||||
gy = resizeStart.value.y
|
||||
let gw = resizeStart.value.width,
|
||||
gh = resizeStart.value.height
|
||||
|
||||
if (handle.includes('e')) gw = Math.max(20, resizeStart.value.width + dx)
|
||||
if (handle.includes('w')) { gw = Math.max(20, resizeStart.value.width - dx); gx = resizeStart.value.x + dx }
|
||||
if (handle.includes('w')) {
|
||||
gw = Math.max(20, resizeStart.value.width - dx)
|
||||
gx = resizeStart.value.x + dx
|
||||
}
|
||||
if (handle.includes('s')) gh = Math.max(10, resizeStart.value.height + dy)
|
||||
if (handle.includes('n')) { gh = Math.max(10, resizeStart.value.height - dy); gy = resizeStart.value.y + dy }
|
||||
if (handle.includes('n')) {
|
||||
gh = Math.max(10, resizeStart.value.height - dy)
|
||||
gy = resizeStart.value.y + dy
|
||||
}
|
||||
|
||||
// Aspect ratio koruma (barkod)
|
||||
if (ar > 0) {
|
||||
@@ -538,7 +601,8 @@ function onResizeMove(e: PointerEvent) {
|
||||
const startHMm = resizeStart.value.height * pxToMm
|
||||
const startXMm = resizeStart.value.x * pxToMm
|
||||
const startYMm = resizeStart.value.y * pxToMm
|
||||
let wMm = startWMm, hMm = startHMm
|
||||
let wMm = startWMm,
|
||||
hMm = startHMm
|
||||
if (handle.includes('e')) {
|
||||
const rightEdge = calculateResizeSnap('right', startXMm + startWMm + dx * pxToMm)
|
||||
wMm = Math.max(5, rightEdge - startXMm)
|
||||
@@ -571,8 +635,10 @@ function onResizeEnd() {
|
||||
const handle = resizeHandle.value
|
||||
const ar = resizeAspectRatio.value
|
||||
const sizeUpdate: { width?: SizeValue; height?: SizeValue } = {}
|
||||
if (handle.includes('e') || handle.includes('w')) sizeUpdate.width = sz.fixed(resizeFinalMm.value.width)
|
||||
if (handle.includes('s') || handle.includes('n')) sizeUpdate.height = sz.fixed(resizeFinalMm.value.height)
|
||||
if (handle.includes('e') || handle.includes('w'))
|
||||
sizeUpdate.width = sz.fixed(resizeFinalMm.value.width)
|
||||
if (handle.includes('s') || handle.includes('n'))
|
||||
sizeUpdate.height = sz.fixed(resizeFinalMm.value.height)
|
||||
// Aspect ratio aktifken her zaman hem width hem height güncelle
|
||||
if (ar > 0) {
|
||||
sizeUpdate.width = sz.fixed(resizeFinalMm.value.width)
|
||||
@@ -621,8 +687,8 @@ function onToolboxDrop(_e: DragEvent) {
|
||||
}
|
||||
|
||||
// Aktif sürükleme var mı (eleman veya toolbox)
|
||||
const isAnyDragActive = computed(() =>
|
||||
(isDragging.value && dragElementId.value !== null) || !!editorStore.draggedNewElement
|
||||
const isAnyDragActive = computed(
|
||||
() => (isDragging.value && dragElementId.value !== null) || !!editorStore.draggedNewElement,
|
||||
)
|
||||
</script>
|
||||
|
||||
@@ -644,26 +710,57 @@ const isAnyDragActive = computed(() =>
|
||||
'element-handle--selected': editorStore.isSelected(el.id),
|
||||
'element-handle--container': isContainer(el),
|
||||
'element-handle--dragging': isDragging && dragElementId === el.id,
|
||||
'element-handle--drop-target': isContainer(el) && dropTargetContainerId === el.id && isAnyDragActive,
|
||||
'element-handle--drop-target':
|
||||
isContainer(el) && dropTargetContainerId === el.id && isAnyDragActive,
|
||||
}"
|
||||
:style="getElementStyle(el)"
|
||||
@pointerdown="(e: PointerEvent) => { onElementClick(e, el.id); onDragStart(e, el) }"
|
||||
@pointerdown="
|
||||
(e: PointerEvent) => {
|
||||
onElementClick(e, el.id)
|
||||
onDragStart(e, el)
|
||||
}
|
||||
"
|
||||
>
|
||||
<!-- Selection border -->
|
||||
<div v-if="editorStore.isSelected(el.id)" class="selection-border" />
|
||||
|
||||
<!-- Resize handles (sadece tek seçimde) -->
|
||||
<template v-if="editorStore.isSelected(el.id) && editorStore.selectedElementIds.size === 1 && !isResizing && el.type !== 'page_break'">
|
||||
<template
|
||||
v-if="
|
||||
editorStore.isSelected(el.id) &&
|
||||
editorStore.selectedElementIds.size === 1 &&
|
||||
!isResizing &&
|
||||
el.type !== 'page_break'
|
||||
"
|
||||
>
|
||||
<template v-if="el.type === 'barcode' || el.type === 'image'">
|
||||
<!-- Barkod/Görsel: sadece yatay resize (aspect ratio korunur) -->
|
||||
<div class="resize-handle resize-handle--e" @pointerdown="(e: PointerEvent) => onResizeStart(e, el.id, 'e')" />
|
||||
<div class="resize-handle resize-handle--w" @pointerdown="(e: PointerEvent) => onResizeStart(e, el.id, 'w')" />
|
||||
<div
|
||||
class="resize-handle resize-handle--e"
|
||||
@pointerdown="(e: PointerEvent) => onResizeStart(e, el.id, 'e')"
|
||||
/>
|
||||
<div
|
||||
class="resize-handle resize-handle--w"
|
||||
@pointerdown="(e: PointerEvent) => onResizeStart(e, el.id, 'w')"
|
||||
/>
|
||||
</template>
|
||||
<template v-else>
|
||||
<div class="resize-handle resize-handle--se" @pointerdown="(e: PointerEvent) => onResizeStart(e, el.id, 'se')" />
|
||||
<div class="resize-handle resize-handle--sw" @pointerdown="(e: PointerEvent) => onResizeStart(e, el.id, 'sw')" />
|
||||
<div class="resize-handle resize-handle--ne" @pointerdown="(e: PointerEvent) => onResizeStart(e, el.id, 'ne')" />
|
||||
<div class="resize-handle resize-handle--nw" @pointerdown="(e: PointerEvent) => onResizeStart(e, el.id, 'nw')" />
|
||||
<div
|
||||
class="resize-handle resize-handle--se"
|
||||
@pointerdown="(e: PointerEvent) => onResizeStart(e, el.id, 'se')"
|
||||
/>
|
||||
<div
|
||||
class="resize-handle resize-handle--sw"
|
||||
@pointerdown="(e: PointerEvent) => onResizeStart(e, el.id, 'sw')"
|
||||
/>
|
||||
<div
|
||||
class="resize-handle resize-handle--ne"
|
||||
@pointerdown="(e: PointerEvent) => onResizeStart(e, el.id, 'ne')"
|
||||
/>
|
||||
<div
|
||||
class="resize-handle resize-handle--nw"
|
||||
@pointerdown="(e: PointerEvent) => onResizeStart(e, el.id, 'nw')"
|
||||
/>
|
||||
</template>
|
||||
</template>
|
||||
</div>
|
||||
@@ -777,12 +874,36 @@ const isAnyDragActive = computed(() =>
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.resize-handle--se { right: -3px; bottom: -3px; cursor: se-resize; }
|
||||
.resize-handle--sw { left: -3px; bottom: -3px; cursor: sw-resize; }
|
||||
.resize-handle--ne { right: -3px; top: -3px; cursor: ne-resize; }
|
||||
.resize-handle--nw { left: -3px; top: -3px; cursor: nw-resize; }
|
||||
.resize-handle--e { right: -3px; top: calc(50% - 3px); cursor: e-resize; }
|
||||
.resize-handle--w { left: -3px; top: calc(50% - 3px); cursor: w-resize; }
|
||||
.resize-handle--se {
|
||||
right: -3px;
|
||||
bottom: -3px;
|
||||
cursor: se-resize;
|
||||
}
|
||||
.resize-handle--sw {
|
||||
left: -3px;
|
||||
bottom: -3px;
|
||||
cursor: sw-resize;
|
||||
}
|
||||
.resize-handle--ne {
|
||||
right: -3px;
|
||||
top: -3px;
|
||||
cursor: ne-resize;
|
||||
}
|
||||
.resize-handle--nw {
|
||||
left: -3px;
|
||||
top: -3px;
|
||||
cursor: nw-resize;
|
||||
}
|
||||
.resize-handle--e {
|
||||
right: -3px;
|
||||
top: calc(50% - 3px);
|
||||
cursor: e-resize;
|
||||
}
|
||||
.resize-handle--w {
|
||||
left: -3px;
|
||||
top: calc(50% - 3px);
|
||||
cursor: w-resize;
|
||||
}
|
||||
|
||||
/* Drag ghost */
|
||||
.drag-ghost {
|
||||
|
||||
@@ -8,7 +8,16 @@ const props = defineProps<{
|
||||
}>()
|
||||
|
||||
// WASM barcode üretme fonksiyonu (EditorCanvas'tan provide edilir)
|
||||
const generateBarcode = inject<(format: string, value: string, width: number, height: number, includeText: boolean) => Promise<{ width: number; height: number; rgba: ArrayBuffer } | null>>('generateBarcode')
|
||||
const generateBarcode =
|
||||
inject<
|
||||
(
|
||||
format: string,
|
||||
value: string,
|
||||
width: number,
|
||||
height: number,
|
||||
includeText: boolean,
|
||||
) => Promise<{ width: number; height: number; rgba: ArrayBuffer } | null>
|
||||
>('generateBarcode')
|
||||
|
||||
function pageContainerStyle(page: PageLayout): Record<string, string> {
|
||||
const s = props.scale
|
||||
@@ -92,7 +101,12 @@ function lineStyle(el: ElementLayout): Record<string, string> {
|
||||
|
||||
// --- Barcode rendering (WASM ile) ---
|
||||
|
||||
async function renderBarcodeToCanvas(canvas: HTMLCanvasElement, format: string, value: string, includeText: boolean = false) {
|
||||
async function renderBarcodeToCanvas(
|
||||
canvas: HTMLCanvasElement,
|
||||
format: string,
|
||||
value: string,
|
||||
includeText: boolean = false,
|
||||
) {
|
||||
if (!value || !generateBarcode) return
|
||||
|
||||
try {
|
||||
@@ -106,7 +120,13 @@ async function renderBarcodeToCanvas(canvas: HTMLCanvasElement, format: string,
|
||||
const hPt = elHmm * MM_TO_PT
|
||||
const size = Math.max(1, Math.round(wPt * 4))
|
||||
const barcodeHeight = Math.max(1, Math.round(hPt * 4))
|
||||
const result = await generateBarcode(format, value, size, barcodeHeight, isQr ? false : includeText)
|
||||
const result = await generateBarcode(
|
||||
format,
|
||||
value,
|
||||
size,
|
||||
barcodeHeight,
|
||||
isQr ? false : includeText,
|
||||
)
|
||||
if (!result) return
|
||||
|
||||
// Canvas boyutlarını WASM çıktısına ayarla (crisp rendering)
|
||||
@@ -116,11 +136,7 @@ async function renderBarcodeToCanvas(canvas: HTMLCanvasElement, format: string,
|
||||
const ctx = canvas.getContext('2d')
|
||||
if (!ctx) return
|
||||
|
||||
const imageData = new ImageData(
|
||||
new Uint8ClampedArray(result.rgba),
|
||||
result.width,
|
||||
result.height,
|
||||
)
|
||||
const imageData = new ImageData(new Uint8ClampedArray(result.rgba), result.width, result.height)
|
||||
ctx.putImageData(imageData, 0, 0)
|
||||
} catch (e) {
|
||||
console.warn(`[dreport] WASM barcode render hatası (${format}):`, e)
|
||||
@@ -159,7 +175,7 @@ watch(
|
||||
await nextTick()
|
||||
await nextTick()
|
||||
const canvases = document.querySelectorAll<HTMLCanvasElement>('canvas[data-barcode]')
|
||||
canvases.forEach(canvas => {
|
||||
canvases.forEach((canvas) => {
|
||||
const format = canvas.dataset.format
|
||||
const value = canvas.dataset.value
|
||||
const includeText = canvas.dataset.includeText === 'true'
|
||||
@@ -168,7 +184,7 @@ watch(
|
||||
}
|
||||
})
|
||||
},
|
||||
{ deep: true }
|
||||
{ deep: true },
|
||||
)
|
||||
</script>
|
||||
|
||||
@@ -187,7 +203,7 @@ watch(
|
||||
class="layout-el layout-el--page-break"
|
||||
:style="elStyle(el)"
|
||||
>
|
||||
<div style="border-top: 1px dashed #9ca3af; width: 100%; height: 0;" />
|
||||
<div style="border-top: 1px dashed #9ca3af; width: 100%; height: 0" />
|
||||
</div>
|
||||
|
||||
<!-- Container -->
|
||||
@@ -200,13 +216,27 @@ watch(
|
||||
}"
|
||||
:style="{ ...elStyle(el), ...containerStyle(el) }"
|
||||
>
|
||||
<span v-if="el.id === 'header' || el.id.startsWith('header_p')" class="layout-el__section-label">Üst Bilgi</span>
|
||||
<span v-else-if="el.id === 'footer' || el.id.startsWith('footer_p')" class="layout-el__section-label">Alt Bilgi</span>
|
||||
<span
|
||||
v-if="el.id === 'header' || el.id.startsWith('header_p')"
|
||||
class="layout-el__section-label"
|
||||
>Üst Bilgi</span
|
||||
>
|
||||
<span
|
||||
v-else-if="el.id === 'footer' || el.id.startsWith('footer_p')"
|
||||
class="layout-el__section-label"
|
||||
>Alt Bilgi</span
|
||||
>
|
||||
</div>
|
||||
|
||||
<!-- Static text / Text / Page number -->
|
||||
<div
|
||||
v-else-if="el.element_type === 'static_text' || el.element_type === 'text' || el.element_type === 'page_number' || el.element_type === 'current_date' || el.element_type === 'calculated_text'"
|
||||
v-else-if="
|
||||
el.element_type === 'static_text' ||
|
||||
el.element_type === 'text' ||
|
||||
el.element_type === 'page_number' ||
|
||||
el.element_type === 'current_date' ||
|
||||
el.element_type === 'calculated_text'
|
||||
"
|
||||
class="layout-el layout-el--text"
|
||||
:style="{ ...elStyle(el), ...textStyle(el) }"
|
||||
>
|
||||
@@ -231,7 +261,11 @@ watch(
|
||||
<img
|
||||
v-if="el.content?.type === 'image' && el.content.src"
|
||||
:src="el.content.src"
|
||||
:style="{ width: '100%', height: '100%', objectFit: (el.style.objectFit || 'fill') as CSSProperties['objectFit'] }"
|
||||
:style="{
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
objectFit: (el.style.objectFit || 'fill') as CSSProperties['objectFit'],
|
||||
}"
|
||||
/>
|
||||
<div v-else class="layout-el__placeholder">Görsel</div>
|
||||
</div>
|
||||
@@ -248,7 +282,10 @@ watch(
|
||||
data-barcode
|
||||
:data-format="el.content.format"
|
||||
:data-value="el.content.value"
|
||||
:data-include-text="el.style.barcodeIncludeText ?? (el.content.format === 'ean13' || el.content.format === 'ean8')"
|
||||
:data-include-text="
|
||||
el.style.barcodeIncludeText ??
|
||||
(el.content.format === 'ean13' || el.content.format === 'ean8')
|
||||
"
|
||||
:data-el-w="el.width_mm"
|
||||
:data-el-h="el.height_mm"
|
||||
:style="{ width: '100%', height: '100%', display: 'block' }"
|
||||
@@ -264,16 +301,24 @@ watch(
|
||||
:style="elStyle(el)"
|
||||
>
|
||||
<svg viewBox="0 0 20 20" :style="{ width: '100%', height: '100%' }">
|
||||
<rect x="1" y="1" width="18" height="18" fill="none"
|
||||
<rect
|
||||
x="1"
|
||||
y="1"
|
||||
width="18"
|
||||
height="18"
|
||||
fill="none"
|
||||
:stroke="el.style.borderColor ?? '#333'"
|
||||
:stroke-width="el.style.borderWidth ? el.style.borderWidth * 3 : 1.5" />
|
||||
<path v-if="el.content?.type === 'checkbox' && el.content.checked"
|
||||
:stroke-width="el.style.borderWidth ? el.style.borderWidth * 3 : 1.5"
|
||||
/>
|
||||
<path
|
||||
v-if="el.content?.type === 'checkbox' && el.content.checked"
|
||||
d="M4 10 L8 15 L16 5"
|
||||
fill="none"
|
||||
:stroke="el.style.color ?? '#000'"
|
||||
stroke-width="2.5"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round" />
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
@@ -293,7 +338,8 @@ watch(
|
||||
fontFamily: span.fontFamily || undefined,
|
||||
color: span.color || undefined,
|
||||
}"
|
||||
>{{ span.text }}</span>
|
||||
>{{ span.text }}</span
|
||||
>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
@@ -313,13 +359,24 @@ watch(
|
||||
<div
|
||||
v-if="el.content?.type === 'chart' && el.content.svg"
|
||||
v-html="el.content.svg"
|
||||
style="width: 100%; height: 100%;"
|
||||
style="width: 100%; height: 100%"
|
||||
/>
|
||||
<div v-else class="layout-el__placeholder" :style="{ display: 'flex', alignItems: 'center', justifyContent: 'center', width: '100%', height: '100%', color: '#94a3b8', fontSize: '12px' }">
|
||||
<div
|
||||
v-else
|
||||
class="layout-el__placeholder"
|
||||
:style="{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
color: '#94a3b8',
|
||||
fontSize: '12px',
|
||||
}"
|
||||
>
|
||||
Grafik
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -21,10 +21,7 @@ const RULER_SIZE = computed(() => props.rulerSize ?? 20)
|
||||
const hCanvas = ref<HTMLCanvasElement | null>(null)
|
||||
const vCanvas = ref<HTMLCanvasElement | null>(null)
|
||||
|
||||
function drawRuler(
|
||||
canvas: HTMLCanvasElement | null,
|
||||
direction: 'horizontal' | 'vertical',
|
||||
) {
|
||||
function drawRuler(canvas: HTMLCanvasElement | null, direction: 'horizontal' | 'vertical') {
|
||||
if (!canvas) return
|
||||
const ctx = canvas.getContext('2d')
|
||||
if (!ctx) return
|
||||
@@ -79,8 +76,9 @@ function drawTicks(
|
||||
// EditorCanvas sayfayı ortalar, ruler da buna uymalı
|
||||
// Yatay: canvas ortası - sayfa genişliği/2
|
||||
// Sayfanın canvas üzerindeki orijin px'i
|
||||
const canvasCenter = direction === 'horizontal'
|
||||
? (length / 2) // flex centering approximation
|
||||
const canvasCenter =
|
||||
direction === 'horizontal'
|
||||
? length / 2 // flex centering approximation
|
||||
: 40 // EditorCanvas padding-top: 40px
|
||||
|
||||
const pageStartPx = canvasCenter - (pageMm * s) / 2 + pan
|
||||
@@ -188,16 +186,8 @@ onBeforeUnmount(() => {
|
||||
|
||||
<template>
|
||||
<div class="ruler-corner" :style="{ width: `${RULER_SIZE}px`, height: `${RULER_SIZE}px` }" />
|
||||
<canvas
|
||||
ref="hCanvas"
|
||||
class="ruler-h"
|
||||
:style="{ height: `${RULER_SIZE}px` }"
|
||||
/>
|
||||
<canvas
|
||||
ref="vCanvas"
|
||||
class="ruler-v"
|
||||
:style="{ width: `${RULER_SIZE}px` }"
|
||||
/>
|
||||
<canvas ref="hCanvas" class="ruler-h" :style="{ height: `${RULER_SIZE}px` }" />
|
||||
<canvas ref="vCanvas" class="ruler-v" :style="{ width: `${RULER_SIZE}px` }" />
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
@@ -55,21 +55,36 @@ const elementTypeLabel = computed(() => {
|
||||
if (el.id === 'header') return 'Üst Bilgi'
|
||||
if (el.id === 'footer') return 'Alt Bilgi'
|
||||
return 'Container'
|
||||
case 'static_text': return 'Metin'
|
||||
case 'text': return 'Metin'
|
||||
case 'line': return 'Cizgi'
|
||||
case 'repeating_table': return 'Tablo'
|
||||
case 'image': return 'Gorsel'
|
||||
case 'page_number': return 'Sayfa No'
|
||||
case 'barcode': return 'Barkod'
|
||||
case 'checkbox': return 'Onay Kutusu'
|
||||
case 'shape': return 'Sekil'
|
||||
case 'current_date': return 'Tarih'
|
||||
case 'calculated_text': return 'Hesaplanan Metin'
|
||||
case 'rich_text': return 'Zengin Metin'
|
||||
case 'page_break': return 'Sayfa Sonu'
|
||||
case 'chart': return 'Grafik'
|
||||
default: return 'Eleman'
|
||||
case 'static_text':
|
||||
return 'Metin'
|
||||
case 'text':
|
||||
return 'Metin'
|
||||
case 'line':
|
||||
return 'Cizgi'
|
||||
case 'repeating_table':
|
||||
return 'Tablo'
|
||||
case 'image':
|
||||
return 'Gorsel'
|
||||
case 'page_number':
|
||||
return 'Sayfa No'
|
||||
case 'barcode':
|
||||
return 'Barkod'
|
||||
case 'checkbox':
|
||||
return 'Onay Kutusu'
|
||||
case 'shape':
|
||||
return 'Sekil'
|
||||
case 'current_date':
|
||||
return 'Tarih'
|
||||
case 'calculated_text':
|
||||
return 'Hesaplanan Metin'
|
||||
case 'rich_text':
|
||||
return 'Zengin Metin'
|
||||
case 'page_break':
|
||||
return 'Sayfa Sonu'
|
||||
case 'chart':
|
||||
return 'Grafik'
|
||||
default:
|
||||
return 'Eleman'
|
||||
}
|
||||
})
|
||||
|
||||
@@ -105,12 +120,12 @@ function deleteSelected() {
|
||||
<div class="properties-panel">
|
||||
<div v-if="multipleSelected" class="properties-panel__empty">
|
||||
{{ editorStore.selectedElementIds.size }} eleman secili
|
||||
<button class="prop-delete-btn" style="margin-top: 12px" @click="deleteSelected">Secilenleri Sil</button>
|
||||
<button class="prop-delete-btn" style="margin-top: 12px" @click="deleteSelected">
|
||||
Secilenleri Sil
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div v-else-if="!selectedElement" class="properties-panel__empty">
|
||||
Bir eleman secin
|
||||
</div>
|
||||
<div v-else-if="!selectedElement" class="properties-panel__empty">Bir eleman secin</div>
|
||||
|
||||
<template v-else>
|
||||
<!-- Header -->
|
||||
@@ -134,68 +149,87 @@ function deleteSelected() {
|
||||
|
||||
<TextProperties
|
||||
v-if="selectedElement.type === 'static_text' || selectedElement.type === 'text'"
|
||||
:element="selectedElement" />
|
||||
:element="selectedElement"
|
||||
/>
|
||||
|
||||
<LineProperties
|
||||
v-if="selectedElement.type === 'line'"
|
||||
:element="(selectedElement as LineElement)" />
|
||||
:element="selectedElement as LineElement"
|
||||
/>
|
||||
|
||||
<ImageProperties
|
||||
v-if="selectedElement.type === 'image'"
|
||||
:element="(selectedElement as ImageElement)" />
|
||||
:element="selectedElement as ImageElement"
|
||||
/>
|
||||
|
||||
<PageNumberProperties
|
||||
v-if="selectedElement.type === 'page_number'"
|
||||
:element="(selectedElement as PageNumberElement)" />
|
||||
:element="selectedElement as PageNumberElement"
|
||||
/>
|
||||
|
||||
<BarcodeProperties
|
||||
v-if="selectedElement.type === 'barcode'"
|
||||
:element="(selectedElement as BarcodeElement)" />
|
||||
:element="selectedElement as BarcodeElement"
|
||||
/>
|
||||
|
||||
<CurrentDateProperties
|
||||
v-if="selectedElement.type === 'current_date'"
|
||||
:element="(selectedElement as CurrentDateElement)" />
|
||||
:element="selectedElement as CurrentDateElement"
|
||||
/>
|
||||
|
||||
<CheckboxProperties
|
||||
v-if="selectedElement.type === 'checkbox'"
|
||||
:element="(selectedElement as CheckboxElement)" />
|
||||
:element="selectedElement as CheckboxElement"
|
||||
/>
|
||||
|
||||
<CalculatedTextProperties
|
||||
v-if="selectedElement.type === 'calculated_text'"
|
||||
:element="(selectedElement as CalculatedTextElement)" />
|
||||
:element="selectedElement as CalculatedTextElement"
|
||||
/>
|
||||
|
||||
<RichTextProperties
|
||||
v-if="selectedElement.type === 'rich_text'"
|
||||
:element="(selectedElement as RichTextElement)" />
|
||||
:element="selectedElement as RichTextElement"
|
||||
/>
|
||||
|
||||
<ShapeProperties
|
||||
v-if="selectedElement.type === 'shape'"
|
||||
:element="(selectedElement as ShapeElement)" />
|
||||
:element="selectedElement as ShapeElement"
|
||||
/>
|
||||
|
||||
<ContainerProperties
|
||||
v-if="isContainer(selectedElement)"
|
||||
:element="(selectedElement as ContainerElement)" />
|
||||
:element="selectedElement as ContainerElement"
|
||||
/>
|
||||
|
||||
<RepeatingTableProperties
|
||||
v-if="selectedElement.type === 'repeating_table'"
|
||||
:element="(selectedElement as RepeatingTableElement)" />
|
||||
:element="selectedElement as RepeatingTableElement"
|
||||
/>
|
||||
|
||||
<ChartProperties
|
||||
v-if="selectedElement.type === 'chart'"
|
||||
:element="(selectedElement as ChartElement)" />
|
||||
:element="selectedElement as ChartElement"
|
||||
/>
|
||||
|
||||
<!-- Header/Footer toggles for root element -->
|
||||
<div v-if="selectedElement.id === 'root'" class="prop-section">
|
||||
<div class="prop-section__title">Sayfa Ust/Alt Bilgi</div>
|
||||
<div class="prop-row">
|
||||
<label class="prop-label">Ust Bilgi (Header)</label>
|
||||
<input type="checkbox" :checked="!!templateStore.template.header"
|
||||
@change="toggleHeader" />
|
||||
<input
|
||||
type="checkbox"
|
||||
:checked="!!templateStore.template.header"
|
||||
@change="toggleHeader"
|
||||
/>
|
||||
</div>
|
||||
<div class="prop-row">
|
||||
<label class="prop-label">Alt Bilgi (Footer)</label>
|
||||
<input type="checkbox" :checked="!!templateStore.template.footer"
|
||||
@change="toggleFooter" />
|
||||
<input
|
||||
type="checkbox"
|
||||
:checked="!!templateStore.template.footer"
|
||||
@change="toggleFooter"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -7,12 +7,15 @@ import { sz } from '../../core/types'
|
||||
import { useEditorStore } from '../../stores/editor'
|
||||
import { useSchemaStore } from '../../stores/schema'
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
node: SchemaNode
|
||||
depth?: number
|
||||
}>(), {
|
||||
}>(),
|
||||
{
|
||||
depth: 0,
|
||||
})
|
||||
},
|
||||
)
|
||||
|
||||
const editorStore = useEditorStore()
|
||||
const schemaStore = useSchemaStore()
|
||||
@@ -68,7 +71,7 @@ function createBoundTextElement(node: SchemaNode): TemplateElement {
|
||||
|
||||
function createBoundTableElement(node: SchemaNode): RepeatingTableElement {
|
||||
const itemFields = schemaStore.getArrayItemFields(node.path)
|
||||
const columns: TableColumn[] = itemFields.map(field => ({
|
||||
const columns: TableColumn[] = itemFields.map((field) => ({
|
||||
id: `col_${(++colIdCounter).toString(36)}`,
|
||||
field: field.key,
|
||||
title: field.title,
|
||||
@@ -108,9 +111,7 @@ function onDragEnd() {
|
||||
editorStore.endDragNewElement()
|
||||
}
|
||||
|
||||
const displayChildren = isArray
|
||||
? (props.node.itemProperties ?? [])
|
||||
: props.node.children
|
||||
const displayChildren = isArray ? (props.node.itemProperties ?? []) : props.node.children
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -131,7 +132,11 @@ const displayChildren = isArray
|
||||
@dragstart="onDragStart"
|
||||
@dragend="onDragEnd"
|
||||
>
|
||||
<span v-if="hasChildren" class="schema-node__arrow" :class="{ 'schema-node__arrow--expanded': expanded }">
|
||||
<span
|
||||
v-if="hasChildren"
|
||||
class="schema-node__arrow"
|
||||
:class="{ 'schema-node__arrow--expanded': expanded }"
|
||||
>
|
||||
▶
|
||||
</span>
|
||||
<span v-else class="schema-node__arrow-placeholder" />
|
||||
|
||||
@@ -1,7 +1,21 @@
|
||||
<script setup lang="ts">
|
||||
import { useEditorStore } from '../../stores/editor'
|
||||
import { useSchemaStore } from '../../stores/schema'
|
||||
import type { TemplateElement, RepeatingTableElement, TableColumn, ImageElement, PageNumberElement, BarcodeElement, PageBreakElement, CurrentDateElement, ShapeElement, CheckboxElement, CalculatedTextElement, RichTextElement, ChartElement } from '../../core/types'
|
||||
import type {
|
||||
TemplateElement,
|
||||
RepeatingTableElement,
|
||||
TableColumn,
|
||||
ImageElement,
|
||||
PageNumberElement,
|
||||
BarcodeElement,
|
||||
PageBreakElement,
|
||||
CurrentDateElement,
|
||||
ShapeElement,
|
||||
CheckboxElement,
|
||||
CalculatedTextElement,
|
||||
RichTextElement,
|
||||
ChartElement,
|
||||
} from '../../core/types'
|
||||
import { sz } from '../../core/types'
|
||||
import { schemaFormatToFormatType, defaultAlignForSchema } from '../../core/schema-parser'
|
||||
|
||||
@@ -88,7 +102,7 @@ const tools: ToolItem[] = [
|
||||
if (firstArray) {
|
||||
dataPath = firstArray.path
|
||||
const itemFields = schemaStore.getArrayItemFields(firstArray.path)
|
||||
columns = itemFields.map(field => ({
|
||||
columns = itemFields.map((field) => ({
|
||||
id: nextId('col'),
|
||||
field: field.key,
|
||||
title: field.title,
|
||||
@@ -212,8 +226,8 @@ const tools: ToolItem[] = [
|
||||
if (firstArray) {
|
||||
dataPath = firstArray.path
|
||||
const itemFields = schemaStore.getArrayItemFields(firstArray.path)
|
||||
const stringField = itemFields.find(f => f.type === 'string')
|
||||
const numberField = itemFields.find(f => f.type === 'number' || f.type === 'integer')
|
||||
const stringField = itemFields.find((f) => f.type === 'string')
|
||||
const numberField = itemFields.find((f) => f.type === 'number' || f.type === 'integer')
|
||||
categoryField = stringField?.key ?? itemFields[0]?.key ?? ''
|
||||
valueField = numberField?.key ?? itemFields[1]?.key ?? ''
|
||||
}
|
||||
|
||||
@@ -50,7 +50,7 @@ function validateBarcode(format: BarcodeFormat, value: string): boolean {
|
||||
case 'code39':
|
||||
return /^[A-Z0-9\-. $/+%]+$/i.test(value)
|
||||
case 'code128':
|
||||
return value.length > 0 && [...value].every(c => c.charCodeAt(0) < 128)
|
||||
return value.length > 0 && [...value].every((c) => c.charCodeAt(0) < 128)
|
||||
case 'qr':
|
||||
return value.length > 0
|
||||
default:
|
||||
@@ -61,10 +61,14 @@ function validateBarcode(format: BarcodeFormat, value: string): boolean {
|
||||
const barcodeInputValue = ref('')
|
||||
const barcodeInputInvalid = ref(false)
|
||||
|
||||
watch(() => props.element.value ?? '', (val) => {
|
||||
watch(
|
||||
() => props.element.value ?? '',
|
||||
(val) => {
|
||||
barcodeInputValue.value = val
|
||||
barcodeInputInvalid.value = false
|
||||
}, { immediate: true })
|
||||
},
|
||||
{ immediate: true },
|
||||
)
|
||||
|
||||
function onBarcodeValueInput(e: Event) {
|
||||
const val = (e.target as HTMLInputElement).value
|
||||
@@ -96,9 +100,13 @@ function onBarcodeFormatChange(newFormat: BarcodeFormat) {
|
||||
<div class="prop-section__title">Barkod Ayarlari</div>
|
||||
<div class="prop-row" data-tip="Barkod formati">
|
||||
<label class="prop-label">Format</label>
|
||||
<select class="prop-input prop-select"
|
||||
<select
|
||||
class="prop-input prop-select"
|
||||
:value="element.format"
|
||||
@change="(e) => onBarcodeFormatChange((e.target as HTMLSelectElement).value as BarcodeFormat)">
|
||||
@change="
|
||||
(e) => onBarcodeFormatChange((e.target as HTMLSelectElement).value as BarcodeFormat)
|
||||
"
|
||||
>
|
||||
<option value="qr">QR Kod</option>
|
||||
<option value="ean13">EAN-13</option>
|
||||
<option value="ean8">EAN-8</option>
|
||||
@@ -108,44 +116,70 @@ function onBarcodeFormatChange(newFormat: BarcodeFormat) {
|
||||
</div>
|
||||
<div class="prop-row" data-tip="Barkod icerigi — formata uygun olmali">
|
||||
<label class="prop-label">Deger</label>
|
||||
<input class="prop-input" type="text"
|
||||
<input
|
||||
class="prop-input"
|
||||
type="text"
|
||||
:class="{ 'prop-input--invalid': barcodeInputInvalid }"
|
||||
:value="barcodeInputValue"
|
||||
@input="onBarcodeValueInput" />
|
||||
@input="onBarcodeValueInput"
|
||||
/>
|
||||
</div>
|
||||
<div class="prop-row" data-tip="Barkod cizgi/modül rengi">
|
||||
<label class="prop-label">Renk</label>
|
||||
<div class="prop-row-inline">
|
||||
<input class="prop-input prop-color" type="color"
|
||||
<input
|
||||
class="prop-input prop-color"
|
||||
type="color"
|
||||
:value="element.style.color ?? '#000000'"
|
||||
@input="(e) => updateStyle('color', (e.target as HTMLInputElement).value)" />
|
||||
<button v-if="element.style.color" class="prop-clear" @click="updateStyle('color', undefined)">x</button>
|
||||
@input="(e) => updateStyle('color', (e.target as HTMLInputElement).value)"
|
||||
/>
|
||||
<button
|
||||
v-if="element.style.color"
|
||||
class="prop-clear"
|
||||
@click="updateStyle('color', undefined)"
|
||||
>
|
||||
x
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="element.format !== 'qr'" class="prop-row" data-tip="Barkod altinda degeri metin olarak goster">
|
||||
<div
|
||||
v-if="element.format !== 'qr'"
|
||||
class="prop-row"
|
||||
data-tip="Barkod altinda degeri metin olarak goster"
|
||||
>
|
||||
<label class="prop-label">Metin Goster</label>
|
||||
<input type="checkbox"
|
||||
:checked="element.style.includeText ?? (element.format === 'ean13' || element.format === 'ean8')"
|
||||
@change="(e) => updateStyle('includeText', (e.target as HTMLInputElement).checked)" />
|
||||
<input
|
||||
type="checkbox"
|
||||
:checked="
|
||||
element.style.includeText ?? (element.format === 'ean13' || element.format === 'ean8')
|
||||
"
|
||||
@change="(e) => updateStyle('includeText', (e.target as HTMLInputElement).checked)"
|
||||
/>
|
||||
</div>
|
||||
<div v-if="schemaStore.scalarFields.length > 0" class="prop-row" data-tip="Schema'dan dinamik veri baglama">
|
||||
<div
|
||||
v-if="schemaStore.scalarFields.length > 0"
|
||||
class="prop-row"
|
||||
data-tip="Schema'dan dinamik veri baglama"
|
||||
>
|
||||
<label class="prop-label">Veri Baglama</label>
|
||||
<select class="prop-input prop-select"
|
||||
<select
|
||||
class="prop-input prop-select"
|
||||
:value="element.binding?.path ?? ''"
|
||||
@change="(e) => {
|
||||
@change="
|
||||
(e) => {
|
||||
const val = (e.target as HTMLSelectElement).value
|
||||
if (val) {
|
||||
update({ binding: { type: 'scalar', path: val } } as any)
|
||||
} else {
|
||||
update({ binding: undefined } as any)
|
||||
}
|
||||
}">
|
||||
}
|
||||
"
|
||||
>
|
||||
<option value="">Yok (statik deger)</option>
|
||||
<option
|
||||
v-for="field in schemaStore.scalarFields"
|
||||
:key="field.path"
|
||||
:value="field.path"
|
||||
>{{ field.title }} ({{ field.path }})</option>
|
||||
<option v-for="field in schemaStore.scalarFields" :key="field.path" :value="field.path">
|
||||
{{ field.title }} ({{ field.path }})
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -27,18 +27,26 @@ function onExpressionChange(value: string) {
|
||||
<template>
|
||||
<div class="prop-section">
|
||||
<div class="prop-section__title">Hesaplanan Metin</div>
|
||||
<div class="prop-row-stack" data-tip="Hesaplama ifadesi (orn: toplamlar.kdv + toplamlar.araToplam)">
|
||||
<div
|
||||
class="prop-row-stack"
|
||||
data-tip="Hesaplama ifadesi (orn: toplamlar.kdv + toplamlar.araToplam)"
|
||||
>
|
||||
<label class="prop-label">Ifade</label>
|
||||
<DexprEditor
|
||||
:model-value="element.expression"
|
||||
@update:model-value="onExpressionChange"
|
||||
placeholder="toplamlar.kdv + toplamlar.araToplam" />
|
||||
placeholder="toplamlar.kdv + toplamlar.araToplam"
|
||||
/>
|
||||
</div>
|
||||
<div class="prop-row" data-tip="Sonucun gosterim formati">
|
||||
<label class="prop-label">Format</label>
|
||||
<select class="prop-input prop-select"
|
||||
<select
|
||||
class="prop-input prop-select"
|
||||
:value="element.format ?? ''"
|
||||
@change="(e) => update({ format: (e.target as HTMLSelectElement).value || undefined } as any)">
|
||||
@change="
|
||||
(e) => update({ format: (e.target as HTMLSelectElement).value || undefined } as any)
|
||||
"
|
||||
>
|
||||
<option value="">Yok</option>
|
||||
<option value="currency">Para Birimi</option>
|
||||
<option value="number">Sayi</option>
|
||||
@@ -47,30 +55,44 @@ function onExpressionChange(value: string) {
|
||||
</div>
|
||||
<div class="prop-row" data-tip="Yazi tipi boyutu (point)">
|
||||
<label class="prop-label">Boyut (pt)</label>
|
||||
<input class="prop-input" type="number" step="1" min="1"
|
||||
<input
|
||||
class="prop-input"
|
||||
type="number"
|
||||
step="1"
|
||||
min="1"
|
||||
:value="(element.style as TextStyle).fontSize ?? 11"
|
||||
@input="(e) => updateStyle('fontSize', parseFloat((e.target as HTMLInputElement).value) || 11)" />
|
||||
@input="
|
||||
(e) => updateStyle('fontSize', parseFloat((e.target as HTMLInputElement).value) || 11)
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
<div class="prop-row" data-tip="Metin rengi">
|
||||
<label class="prop-label">Renk</label>
|
||||
<input class="prop-input prop-color" type="color"
|
||||
<input
|
||||
class="prop-input prop-color"
|
||||
type="color"
|
||||
:value="(element.style as TextStyle).color ?? '#000000'"
|
||||
@input="(e) => updateStyle('color', (e.target as HTMLInputElement).value)" />
|
||||
@input="(e) => updateStyle('color', (e.target as HTMLInputElement).value)"
|
||||
/>
|
||||
</div>
|
||||
<div class="prop-row" data-tip="Yazi tipi kalinligi">
|
||||
<label class="prop-label">Kalinlik</label>
|
||||
<select class="prop-input prop-select"
|
||||
<select
|
||||
class="prop-input prop-select"
|
||||
:value="(element.style as TextStyle).fontWeight ?? 'normal'"
|
||||
@change="(e) => updateStyle('fontWeight', (e.target as HTMLSelectElement).value)">
|
||||
@change="(e) => updateStyle('fontWeight', (e.target as HTMLSelectElement).value)"
|
||||
>
|
||||
<option value="normal">Normal</option>
|
||||
<option value="bold">Kalin</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="prop-row" data-tip="Metnin yatay hizalamasi">
|
||||
<label class="prop-label">Hizalama</label>
|
||||
<select class="prop-input prop-select"
|
||||
<select
|
||||
class="prop-input prop-select"
|
||||
:value="(element.style as TextStyle).align ?? 'left'"
|
||||
@change="(e) => updateStyle('align', (e.target as HTMLSelectElement).value)">
|
||||
@change="(e) => updateStyle('align', (e.target as HTMLSelectElement).value)"
|
||||
>
|
||||
<option value="left">Sol</option>
|
||||
<option value="center">Orta</option>
|
||||
<option value="right">Sag</option>
|
||||
|
||||
@@ -33,13 +33,15 @@ const itemFields = computed(() => {
|
||||
return schemaStore.getArrayItemFields(path)
|
||||
})
|
||||
|
||||
const stringFields = computed(() => itemFields.value.filter(f => f.type === 'string'))
|
||||
const numberFields = computed(() => itemFields.value.filter(f => f.type === 'number' || f.type === 'integer'))
|
||||
const stringFields = computed(() => itemFields.value.filter((f) => f.type === 'string'))
|
||||
const numberFields = computed(() =>
|
||||
itemFields.value.filter((f) => f.type === 'number' || f.type === 'integer'),
|
||||
)
|
||||
|
||||
function updateDataSource(path: string) {
|
||||
const fields = schemaStore.getArrayItemFields(path)
|
||||
const strField = fields.find(f => f.type === 'string')
|
||||
const numField = fields.find(f => f.type === 'number' || f.type === 'integer')
|
||||
const strField = fields.find((f) => f.type === 'string')
|
||||
const numField = fields.find((f) => f.type === 'number' || f.type === 'integer')
|
||||
update({
|
||||
dataSource: { type: 'array', path },
|
||||
categoryField: strField?.key ?? fields[0]?.key ?? '',
|
||||
@@ -73,7 +75,9 @@ const hasGroup = computed(() => !!props.element.groupField)
|
||||
|
||||
// Renk paleti (default 6 renk)
|
||||
const colorList = computed(() => {
|
||||
return props.element.style.colors ?? ['#4F46E5', '#10B981', '#F59E0B', '#EF4444', '#8B5CF6', '#EC4899']
|
||||
return (
|
||||
props.element.style.colors ?? ['#4F46E5', '#10B981', '#F59E0B', '#EF4444', '#8B5CF6', '#EC4899']
|
||||
)
|
||||
})
|
||||
|
||||
function updateColor(index: number, value: string) {
|
||||
@@ -99,7 +103,11 @@ function removeColor(index: number) {
|
||||
<div class="prop-section">
|
||||
<div class="prop-section__title">Grafik Tipi</div>
|
||||
<div class="prop-row">
|
||||
<select class="prop-input prop-select" :value="element.chartType" @change="update({ chartType: ($event.target as HTMLSelectElement).value as ChartType })">
|
||||
<select
|
||||
class="prop-input prop-select"
|
||||
:value="element.chartType"
|
||||
@change="update({ chartType: ($event.target as HTMLSelectElement).value as ChartType })"
|
||||
>
|
||||
<option value="bar">Bar</option>
|
||||
<option value="line">Line</option>
|
||||
<option value="pie">Pie</option>
|
||||
@@ -112,33 +120,61 @@ function removeColor(index: number) {
|
||||
<div class="prop-section__title">Veri Kaynagi</div>
|
||||
<div class="prop-row">
|
||||
<label class="prop-label">Array</label>
|
||||
<select class="prop-input prop-select" :value="element.dataSource?.path ?? ''" @change="updateDataSource(($event.target as HTMLSelectElement).value)">
|
||||
<select
|
||||
class="prop-input prop-select"
|
||||
:value="element.dataSource?.path ?? ''"
|
||||
@change="updateDataSource(($event.target as HTMLSelectElement).value)"
|
||||
>
|
||||
<option value="" disabled>Sec...</option>
|
||||
<option v-for="arr in arrayFields" :key="arr.path" :value="arr.path">{{ arr.title || arr.path }}</option>
|
||||
<option v-for="arr in arrayFields" :key="arr.path" :value="arr.path">
|
||||
{{ arr.title || arr.path }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="prop-row">
|
||||
<label class="prop-label">Kategori</label>
|
||||
<select class="prop-input prop-select" :value="element.categoryField" @change="update({ categoryField: ($event.target as HTMLSelectElement).value })">
|
||||
<option v-for="f in itemFields" :key="f.key" :value="f.key">{{ f.title || f.key }}</option>
|
||||
<select
|
||||
class="prop-input prop-select"
|
||||
:value="element.categoryField"
|
||||
@change="update({ categoryField: ($event.target as HTMLSelectElement).value })"
|
||||
>
|
||||
<option v-for="f in itemFields" :key="f.key" :value="f.key">
|
||||
{{ f.title || f.key }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="prop-row">
|
||||
<label class="prop-label">Deger</label>
|
||||
<select class="prop-input prop-select" :value="element.valueField" @change="update({ valueField: ($event.target as HTMLSelectElement).value })">
|
||||
<option v-for="f in numberFields" :key="f.key" :value="f.key">{{ f.title || f.key }}</option>
|
||||
<select
|
||||
class="prop-input prop-select"
|
||||
:value="element.valueField"
|
||||
@change="update({ valueField: ($event.target as HTMLSelectElement).value })"
|
||||
>
|
||||
<option v-for="f in numberFields" :key="f.key" :value="f.key">
|
||||
{{ f.title || f.key }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="prop-row">
|
||||
<label class="prop-label">Gruplama</label>
|
||||
<select class="prop-input prop-select" :value="element.groupField ?? ''" @change="update({ groupField: ($event.target as HTMLSelectElement).value || undefined })">
|
||||
<select
|
||||
class="prop-input prop-select"
|
||||
:value="element.groupField ?? ''"
|
||||
@change="update({ groupField: ($event.target as HTMLSelectElement).value || undefined })"
|
||||
>
|
||||
<option value="">Yok</option>
|
||||
<option v-for="f in stringFields" :key="f.key" :value="f.key">{{ f.title || f.key }}</option>
|
||||
<option v-for="f in stringFields" :key="f.key" :value="f.key">
|
||||
{{ f.title || f.key }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
<div v-if="hasGroup && !isPie" class="prop-row">
|
||||
<label class="prop-label">Grup Modu</label>
|
||||
<select class="prop-input prop-select" :value="element.groupMode ?? 'grouped'" @change="update({ groupMode: ($event.target as HTMLSelectElement).value as GroupMode })">
|
||||
<select
|
||||
class="prop-input prop-select"
|
||||
:value="element.groupMode ?? 'grouped'"
|
||||
@change="update({ groupMode: ($event.target as HTMLSelectElement).value as GroupMode })"
|
||||
>
|
||||
<option value="grouped">Yan Yana</option>
|
||||
<option value="stacked">Yigin</option>
|
||||
</select>
|
||||
@@ -150,19 +186,40 @@ function removeColor(index: number) {
|
||||
<div class="prop-section__title">Baslik</div>
|
||||
<div class="prop-row">
|
||||
<label class="prop-label">Metin</label>
|
||||
<input class="prop-input" type="text" :value="element.title?.text ?? ''" @change="updateTitle('text', ($event.target as HTMLInputElement).value)" placeholder="Grafik basligi">
|
||||
<input
|
||||
class="prop-input"
|
||||
type="text"
|
||||
:value="element.title?.text ?? ''"
|
||||
@change="updateTitle('text', ($event.target as HTMLInputElement).value)"
|
||||
placeholder="Grafik basligi"
|
||||
/>
|
||||
</div>
|
||||
<div class="prop-row" v-if="element.title?.text">
|
||||
<label class="prop-label">Boyut</label>
|
||||
<input class="prop-input prop-input--sm" type="number" :value="element.title?.fontSize ?? 4" step="0.5" @change="updateTitle('fontSize', parseFloat(($event.target as HTMLInputElement).value))">
|
||||
<input
|
||||
class="prop-input prop-input--sm"
|
||||
type="number"
|
||||
:value="element.title?.fontSize ?? 4"
|
||||
step="0.5"
|
||||
@change="updateTitle('fontSize', parseFloat(($event.target as HTMLInputElement).value))"
|
||||
/>
|
||||
</div>
|
||||
<div class="prop-row" v-if="element.title?.text">
|
||||
<label class="prop-label">Renk</label>
|
||||
<input class="prop-color" type="color" :value="element.title?.color ?? '#333333'" @input="updateTitle('color', ($event.target as HTMLInputElement).value)">
|
||||
<input
|
||||
class="prop-color"
|
||||
type="color"
|
||||
:value="element.title?.color ?? '#333333'"
|
||||
@input="updateTitle('color', ($event.target as HTMLInputElement).value)"
|
||||
/>
|
||||
</div>
|
||||
<div class="prop-row" v-if="element.title?.text">
|
||||
<label class="prop-label">Hiza</label>
|
||||
<select class="prop-input prop-select" :value="element.title?.align ?? 'center'" @change="updateTitle('align', ($event.target as HTMLSelectElement).value)">
|
||||
<select
|
||||
class="prop-input prop-select"
|
||||
:value="element.title?.align ?? 'center'"
|
||||
@change="updateTitle('align', ($event.target as HTMLSelectElement).value)"
|
||||
>
|
||||
<option value="left">Sol</option>
|
||||
<option value="center">Orta</option>
|
||||
<option value="right">Sag</option>
|
||||
@@ -175,12 +232,20 @@ function removeColor(index: number) {
|
||||
<div class="prop-section__title">Gosterge</div>
|
||||
<div class="prop-row">
|
||||
<label class="prop-label">Goster</label>
|
||||
<input type="checkbox" :checked="element.legend?.show ?? false" @change="updateLegend('show', ($event.target as HTMLInputElement).checked)">
|
||||
<input
|
||||
type="checkbox"
|
||||
:checked="element.legend?.show ?? false"
|
||||
@change="updateLegend('show', ($event.target as HTMLInputElement).checked)"
|
||||
/>
|
||||
</div>
|
||||
<template v-if="element.legend?.show">
|
||||
<div class="prop-row">
|
||||
<label class="prop-label">Konum</label>
|
||||
<select class="prop-input prop-select" :value="element.legend?.position ?? 'bottom'" @change="updateLegend('position', ($event.target as HTMLSelectElement).value)">
|
||||
<select
|
||||
class="prop-input prop-select"
|
||||
:value="element.legend?.position ?? 'bottom'"
|
||||
@change="updateLegend('position', ($event.target as HTMLSelectElement).value)"
|
||||
>
|
||||
<option value="top">Ust</option>
|
||||
<option value="bottom">Alt</option>
|
||||
<option value="right">Sag</option>
|
||||
@@ -188,7 +253,15 @@ function removeColor(index: number) {
|
||||
</div>
|
||||
<div class="prop-row">
|
||||
<label class="prop-label">Boyut</label>
|
||||
<input class="prop-input prop-input--sm" type="number" :value="element.legend?.fontSize ?? 2.8" step="0.2" @change="updateLegend('fontSize', parseFloat(($event.target as HTMLInputElement).value))">
|
||||
<input
|
||||
class="prop-input prop-input--sm"
|
||||
type="number"
|
||||
:value="element.legend?.fontSize ?? 2.8"
|
||||
step="0.2"
|
||||
@change="
|
||||
updateLegend('fontSize', parseFloat(($event.target as HTMLInputElement).value))
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
@@ -198,16 +271,33 @@ function removeColor(index: number) {
|
||||
<div class="prop-section__title">Etiketler</div>
|
||||
<div class="prop-row">
|
||||
<label class="prop-label">Goster</label>
|
||||
<input type="checkbox" :checked="element.labels?.show ?? false" @change="updateLabels('show', ($event.target as HTMLInputElement).checked)">
|
||||
<input
|
||||
type="checkbox"
|
||||
:checked="element.labels?.show ?? false"
|
||||
@change="updateLabels('show', ($event.target as HTMLInputElement).checked)"
|
||||
/>
|
||||
</div>
|
||||
<template v-if="element.labels?.show">
|
||||
<div class="prop-row">
|
||||
<label class="prop-label">Boyut</label>
|
||||
<input class="prop-input prop-input--sm" type="number" :value="element.labels?.fontSize ?? 2.2" step="0.2" @change="updateLabels('fontSize', parseFloat(($event.target as HTMLInputElement).value))">
|
||||
<input
|
||||
class="prop-input prop-input--sm"
|
||||
type="number"
|
||||
:value="element.labels?.fontSize ?? 2.2"
|
||||
step="0.2"
|
||||
@change="
|
||||
updateLabels('fontSize', parseFloat(($event.target as HTMLInputElement).value))
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
<div class="prop-row">
|
||||
<label class="prop-label">Renk</label>
|
||||
<input class="prop-color" type="color" :value="element.labels?.color ?? '#333333'" @input="updateLabels('color', ($event.target as HTMLInputElement).value)">
|
||||
<input
|
||||
class="prop-color"
|
||||
type="color"
|
||||
:value="element.labels?.color ?? '#333333'"
|
||||
@input="updateLabels('color', ($event.target as HTMLInputElement).value)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
@@ -217,19 +307,40 @@ function removeColor(index: number) {
|
||||
<div class="prop-section__title">Eksenler</div>
|
||||
<div class="prop-row">
|
||||
<label class="prop-label">X Etiketi</label>
|
||||
<input class="prop-input" type="text" :value="element.axis?.xLabel ?? ''" @change="updateAxis('xLabel', ($event.target as HTMLInputElement).value || undefined)" placeholder="X ekseni">
|
||||
<input
|
||||
class="prop-input"
|
||||
type="text"
|
||||
:value="element.axis?.xLabel ?? ''"
|
||||
@change="updateAxis('xLabel', ($event.target as HTMLInputElement).value || undefined)"
|
||||
placeholder="X ekseni"
|
||||
/>
|
||||
</div>
|
||||
<div class="prop-row">
|
||||
<label class="prop-label">Y Etiketi</label>
|
||||
<input class="prop-input" type="text" :value="element.axis?.yLabel ?? ''" @change="updateAxis('yLabel', ($event.target as HTMLInputElement).value || undefined)" placeholder="Y ekseni">
|
||||
<input
|
||||
class="prop-input"
|
||||
type="text"
|
||||
:value="element.axis?.yLabel ?? ''"
|
||||
@change="updateAxis('yLabel', ($event.target as HTMLInputElement).value || undefined)"
|
||||
placeholder="Y ekseni"
|
||||
/>
|
||||
</div>
|
||||
<div class="prop-row">
|
||||
<label class="prop-label">Izgara</label>
|
||||
<input type="checkbox" :checked="element.axis?.showGrid ?? true" @change="updateAxis('showGrid', ($event.target as HTMLInputElement).checked)">
|
||||
<input
|
||||
type="checkbox"
|
||||
:checked="element.axis?.showGrid ?? true"
|
||||
@change="updateAxis('showGrid', ($event.target as HTMLInputElement).checked)"
|
||||
/>
|
||||
</div>
|
||||
<div class="prop-row" v-if="element.axis?.showGrid !== false">
|
||||
<label class="prop-label">Izgara Renk</label>
|
||||
<input class="prop-color" type="color" :value="element.axis?.gridColor ?? '#E5E7EB'" @input="updateAxis('gridColor', ($event.target as HTMLInputElement).value)">
|
||||
<input
|
||||
class="prop-color"
|
||||
type="color"
|
||||
:value="element.axis?.gridColor ?? '#E5E7EB'"
|
||||
@input="updateAxis('gridColor', ($event.target as HTMLInputElement).value)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -238,14 +349,26 @@ function removeColor(index: number) {
|
||||
<div class="prop-section__title">Stil</div>
|
||||
<div class="prop-row">
|
||||
<label class="prop-label">Arka Plan</label>
|
||||
<input class="prop-color" type="color" :value="element.style.backgroundColor ?? '#FFFFFF'" @input="updateStyle('backgroundColor', ($event.target as HTMLInputElement).value)">
|
||||
<input
|
||||
class="prop-color"
|
||||
type="color"
|
||||
:value="element.style.backgroundColor ?? '#FFFFFF'"
|
||||
@input="updateStyle('backgroundColor', ($event.target as HTMLInputElement).value)"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Renk Paleti -->
|
||||
<div class="prop-section__subtitle">Renk Paleti</div>
|
||||
<div v-for="(color, i) in colorList" :key="i" class="prop-row">
|
||||
<input class="prop-color" type="color" :value="color" @input="updateColor(i, ($event.target as HTMLInputElement).value)">
|
||||
<button class="prop-btn-sm prop-btn-sm--danger" @click="removeColor(i)" title="Kaldir">×</button>
|
||||
<input
|
||||
class="prop-color"
|
||||
type="color"
|
||||
:value="color"
|
||||
@input="updateColor(i, ($event.target as HTMLInputElement).value)"
|
||||
/>
|
||||
<button class="prop-btn-sm prop-btn-sm--danger" @click="removeColor(i)" title="Kaldir">
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
<button class="prop-btn-sm" @click="addColor">+ Renk Ekle</button>
|
||||
</div>
|
||||
@@ -255,7 +378,15 @@ function removeColor(index: number) {
|
||||
<div class="prop-section__title">Bar Ayarlari</div>
|
||||
<div class="prop-row">
|
||||
<label class="prop-label">Bar Boslugu</label>
|
||||
<input class="prop-input prop-input--sm" type="number" :value="element.style.barGap ?? 0.2" step="0.05" min="0" max="0.8" @change="updateStyle('barGap', parseFloat(($event.target as HTMLInputElement).value))">
|
||||
<input
|
||||
class="prop-input prop-input--sm"
|
||||
type="number"
|
||||
:value="element.style.barGap ?? 0.2"
|
||||
step="0.05"
|
||||
min="0"
|
||||
max="0.8"
|
||||
@change="updateStyle('barGap', parseFloat(($event.target as HTMLInputElement).value))"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -263,11 +394,22 @@ function removeColor(index: number) {
|
||||
<div class="prop-section__title">Line Ayarlari</div>
|
||||
<div class="prop-row">
|
||||
<label class="prop-label">Cizgi Kalinligi</label>
|
||||
<input class="prop-input prop-input--sm" type="number" :value="element.style.lineWidth ?? 0.5" step="0.1" min="0.1" @change="updateStyle('lineWidth', parseFloat(($event.target as HTMLInputElement).value))">
|
||||
<input
|
||||
class="prop-input prop-input--sm"
|
||||
type="number"
|
||||
:value="element.style.lineWidth ?? 0.5"
|
||||
step="0.1"
|
||||
min="0.1"
|
||||
@change="updateStyle('lineWidth', parseFloat(($event.target as HTMLInputElement).value))"
|
||||
/>
|
||||
</div>
|
||||
<div class="prop-row">
|
||||
<label class="prop-label">Noktalar</label>
|
||||
<input type="checkbox" :checked="element.style.showPoints ?? true" @change="updateStyle('showPoints', ($event.target as HTMLInputElement).checked)">
|
||||
<input
|
||||
type="checkbox"
|
||||
:checked="element.style.showPoints ?? true"
|
||||
@change="updateStyle('showPoints', ($event.target as HTMLInputElement).checked)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -275,11 +417,19 @@ function removeColor(index: number) {
|
||||
<div class="prop-section__title">Pie Ayarlari</div>
|
||||
<div class="prop-row">
|
||||
<label class="prop-label">Ic Yaricap</label>
|
||||
<input class="prop-input prop-input--sm" type="number" :value="element.style.innerRadius ?? 0" step="0.05" min="0" max="0.9" @change="updateStyle('innerRadius', parseFloat(($event.target as HTMLInputElement).value))">
|
||||
</div>
|
||||
<div class="prop-row" style="font-size: 11px; color: #94a3b8;">
|
||||
0 = Pie, >0 = Donut
|
||||
<input
|
||||
class="prop-input prop-input--sm"
|
||||
type="number"
|
||||
:value="element.style.innerRadius ?? 0"
|
||||
step="0.05"
|
||||
min="0"
|
||||
max="0.9"
|
||||
@change="
|
||||
updateStyle('innerRadius', parseFloat(($event.target as HTMLInputElement).value))
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
<div class="prop-row" style="font-size: 11px; color: #94a3b8">0 = Pie, >0 = Donut</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -24,27 +24,40 @@ function updateStyle(key: string, value: unknown) {
|
||||
<div class="prop-section__title">Onay Kutusu</div>
|
||||
<div v-if="!element.binding" class="prop-row" data-tip="Onay kutusunun varsayilan durumu">
|
||||
<label class="prop-label">Isaretli</label>
|
||||
<input type="checkbox"
|
||||
<input
|
||||
type="checkbox"
|
||||
:checked="element.checked ?? false"
|
||||
@change="(e) => update({ checked: (e.target as HTMLInputElement).checked } as any)" />
|
||||
@change="(e) => update({ checked: (e.target as HTMLInputElement).checked } as any)"
|
||||
/>
|
||||
</div>
|
||||
<div class="prop-row" data-tip="Onay kutusu boyutu (mm)">
|
||||
<label class="prop-label">Boyut (mm)</label>
|
||||
<input class="prop-input" type="number" step="0.5" min="1"
|
||||
<input
|
||||
class="prop-input"
|
||||
type="number"
|
||||
step="0.5"
|
||||
min="1"
|
||||
:value="element.style.size ?? 4"
|
||||
@input="(e) => updateStyle('size', parseFloat((e.target as HTMLInputElement).value) || 4)" />
|
||||
@input="(e) => updateStyle('size', parseFloat((e.target as HTMLInputElement).value) || 4)"
|
||||
/>
|
||||
</div>
|
||||
<div class="prop-row" data-tip="Isaret (tik) rengi">
|
||||
<label class="prop-label">Isaret Rengi</label>
|
||||
<input class="prop-input prop-color" type="color"
|
||||
<input
|
||||
class="prop-input prop-color"
|
||||
type="color"
|
||||
:value="element.style.checkColor ?? '#000000'"
|
||||
@input="(e) => updateStyle('checkColor', (e.target as HTMLInputElement).value)" />
|
||||
@input="(e) => updateStyle('checkColor', (e.target as HTMLInputElement).value)"
|
||||
/>
|
||||
</div>
|
||||
<div class="prop-row" data-tip="Kutu kenarlik rengi">
|
||||
<label class="prop-label">Kenar Rengi</label>
|
||||
<input class="prop-input prop-color" type="color"
|
||||
<input
|
||||
class="prop-input prop-color"
|
||||
type="color"
|
||||
:value="element.style.borderColor ?? '#333333'"
|
||||
@input="(e) => updateStyle('borderColor', (e.target as HTMLInputElement).value)" />
|
||||
@input="(e) => updateStyle('borderColor', (e.target as HTMLInputElement).value)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -25,24 +25,37 @@ function updateStyle(key: string, value: unknown) {
|
||||
<div class="prop-section__title">Container Ayarlari</div>
|
||||
<div class="prop-row" data-tip="Cocuk elemanlarin dizilim yonu">
|
||||
<label class="prop-label">Yon</label>
|
||||
<select class="prop-input prop-select"
|
||||
<select
|
||||
class="prop-input prop-select"
|
||||
:value="element.direction"
|
||||
@change="(e) => update({ direction: (e.target as HTMLSelectElement).value } as any)">
|
||||
@change="(e) => update({ direction: (e.target as HTMLSelectElement).value } as any)"
|
||||
>
|
||||
<option value="column">Dikey</option>
|
||||
<option value="row">Yatay</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="prop-row" data-tip="Cocuk elemanlar arasi bosluk (mm)">
|
||||
<label class="prop-label">Bosluk (mm)</label>
|
||||
<input class="prop-input" type="number" step="1" min="0"
|
||||
<input
|
||||
class="prop-input"
|
||||
type="number"
|
||||
step="1"
|
||||
min="0"
|
||||
:value="element.gap"
|
||||
@input="(e) => update({ gap: parseFloat((e.target as HTMLInputElement).value) || 0 } as any)" />
|
||||
@input="
|
||||
(e) => update({ gap: parseFloat((e.target as HTMLInputElement).value) || 0 } as any)
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
<div class="prop-row" data-tip="Cocuklarin cross-axis hizalamasi">
|
||||
<label class="prop-label">{{ element.direction === 'column' ? 'Yatay Hizalama' : 'Dikey Hizalama' }}</label>
|
||||
<select class="prop-input prop-select"
|
||||
<label class="prop-label">{{
|
||||
element.direction === 'column' ? 'Yatay Hizalama' : 'Dikey Hizalama'
|
||||
}}</label>
|
||||
<select
|
||||
class="prop-input prop-select"
|
||||
:value="element.align"
|
||||
@change="(e) => update({ align: (e.target as HTMLSelectElement).value } as any)">
|
||||
@change="(e) => update({ align: (e.target as HTMLSelectElement).value } as any)"
|
||||
>
|
||||
<option value="start">{{ element.direction === 'column' ? 'Sol' : 'Ust' }}</option>
|
||||
<option value="center">Orta</option>
|
||||
<option value="end">{{ element.direction === 'column' ? 'Sag' : 'Alt' }}</option>
|
||||
@@ -50,10 +63,14 @@ function updateStyle(key: string, value: unknown) {
|
||||
</select>
|
||||
</div>
|
||||
<div class="prop-row" data-tip="Cocuklarin main-axis dagilimi">
|
||||
<label class="prop-label">{{ element.direction === 'column' ? 'Dikey Dagilim' : 'Yatay Dagilim' }}</label>
|
||||
<select class="prop-input prop-select"
|
||||
<label class="prop-label">{{
|
||||
element.direction === 'column' ? 'Dikey Dagilim' : 'Yatay Dagilim'
|
||||
}}</label>
|
||||
<select
|
||||
class="prop-input prop-select"
|
||||
:value="element.justify"
|
||||
@change="(e) => update({ justify: (e.target as HTMLSelectElement).value } as any)">
|
||||
@change="(e) => update({ justify: (e.target as HTMLSelectElement).value } as any)"
|
||||
>
|
||||
<option value="start">{{ element.direction === 'column' ? 'Ust' : 'Sol' }}</option>
|
||||
<option value="center">Orta</option>
|
||||
<option value="end">{{ element.direction === 'column' ? 'Alt' : 'Sag' }}</option>
|
||||
@@ -72,9 +89,11 @@ function updateStyle(key: string, value: unknown) {
|
||||
|
||||
<div class="prop-row" data-tip="Sayfa sonunda bolunmeyi kontrol eder">
|
||||
<label class="prop-label">Sayfa Bolme</label>
|
||||
<select class="prop-input prop-select"
|
||||
<select
|
||||
class="prop-input prop-select"
|
||||
:value="element.breakInside ?? 'auto'"
|
||||
@change="(e) => update({ breakInside: (e.target as HTMLSelectElement).value } as any)">
|
||||
@change="(e) => update({ breakInside: (e.target as HTMLSelectElement).value } as any)"
|
||||
>
|
||||
<option value="auto">Izin Ver</option>
|
||||
<option value="avoid">Bolme</option>
|
||||
</select>
|
||||
@@ -84,32 +103,59 @@ function updateStyle(key: string, value: unknown) {
|
||||
<div class="prop-row" data-tip="Container arka plan rengi">
|
||||
<label class="prop-label">Arka plan</label>
|
||||
<div class="prop-row-inline">
|
||||
<input class="prop-input prop-color" type="color"
|
||||
<input
|
||||
class="prop-input prop-color"
|
||||
type="color"
|
||||
:value="element.style.backgroundColor ?? '#ffffff'"
|
||||
@input="(e) => updateStyle('backgroundColor', (e.target as HTMLInputElement).value)" />
|
||||
<button v-if="element.style.backgroundColor" class="prop-clear" @click="updateStyle('backgroundColor', undefined)">x</button>
|
||||
@input="(e) => updateStyle('backgroundColor', (e.target as HTMLInputElement).value)"
|
||||
/>
|
||||
<button
|
||||
v-if="element.style.backgroundColor"
|
||||
class="prop-clear"
|
||||
@click="updateStyle('backgroundColor', undefined)"
|
||||
>
|
||||
x
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="prop-row" data-tip="Kenarlik kalinligi (mm)">
|
||||
<label class="prop-label">Kenarlik (mm)</label>
|
||||
<input class="prop-input" type="number" step="0.1" min="0"
|
||||
<input
|
||||
class="prop-input"
|
||||
type="number"
|
||||
step="0.1"
|
||||
min="0"
|
||||
:value="element.style.borderWidth ?? 0"
|
||||
@input="(e) => updateStyle('borderWidth', parseFloat((e.target as HTMLInputElement).value) || 0)" />
|
||||
@input="
|
||||
(e) => updateStyle('borderWidth', parseFloat((e.target as HTMLInputElement).value) || 0)
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
<div class="prop-row" data-tip="Kenarlik cizgisi rengi">
|
||||
<label class="prop-label">Kenarlik rengi</label>
|
||||
<div class="prop-row-inline">
|
||||
<input class="prop-input prop-color" type="color"
|
||||
<input
|
||||
class="prop-input prop-color"
|
||||
type="color"
|
||||
:value="element.style.borderColor ?? '#000000'"
|
||||
@input="(e) => updateStyle('borderColor', (e.target as HTMLInputElement).value)" />
|
||||
<button v-if="element.style.borderColor" class="prop-clear" @click="updateStyle('borderColor', undefined)">x</button>
|
||||
@input="(e) => updateStyle('borderColor', (e.target as HTMLInputElement).value)"
|
||||
/>
|
||||
<button
|
||||
v-if="element.style.borderColor"
|
||||
class="prop-clear"
|
||||
@click="updateStyle('borderColor', undefined)"
|
||||
>
|
||||
x
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="prop-row" data-tip="Kenarlik cizgi stili">
|
||||
<label class="prop-label">Kenarlik stili</label>
|
||||
<select class="prop-input prop-select"
|
||||
<select
|
||||
class="prop-input prop-select"
|
||||
:value="element.style.borderStyle ?? 'solid'"
|
||||
@change="(e) => updateStyle('borderStyle', (e.target as HTMLSelectElement).value)">
|
||||
@change="(e) => updateStyle('borderStyle', (e.target as HTMLSelectElement).value)"
|
||||
>
|
||||
<option value="solid">Duz</option>
|
||||
<option value="dashed">Kesikli</option>
|
||||
<option value="dotted">Noktali</option>
|
||||
@@ -117,9 +163,16 @@ function updateStyle(key: string, value: unknown) {
|
||||
</div>
|
||||
<div class="prop-row" data-tip="Kose yuvarlakligi (mm)">
|
||||
<label class="prop-label">Radius (mm)</label>
|
||||
<input class="prop-input" type="number" step="0.5" min="0"
|
||||
<input
|
||||
class="prop-input"
|
||||
type="number"
|
||||
step="0.5"
|
||||
min="0"
|
||||
:value="element.style.borderRadius ?? 0"
|
||||
@input="(e) => updateStyle('borderRadius', parseFloat((e.target as HTMLInputElement).value) || 0)" />
|
||||
@input="
|
||||
(e) => updateStyle('borderRadius', parseFloat((e.target as HTMLInputElement).value) || 0)
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -24,9 +24,11 @@ function updateStyle(key: string, value: unknown) {
|
||||
<div class="prop-section__title">Tarih</div>
|
||||
<div class="prop-row" data-tip="Tarih gosterim formati">
|
||||
<label class="prop-label">Format</label>
|
||||
<select class="prop-input prop-select"
|
||||
<select
|
||||
class="prop-input prop-select"
|
||||
:value="element.format ?? 'DD.MM.YYYY'"
|
||||
@change="(e) => update({ format: (e.target as HTMLSelectElement).value } as any)">
|
||||
@change="(e) => update({ format: (e.target as HTMLSelectElement).value } as any)"
|
||||
>
|
||||
<option value="DD.MM.YYYY">30.03.2026</option>
|
||||
<option value="DD/MM/YYYY">30/03/2026</option>
|
||||
<option value="YYYY-MM-DD">2026-03-30</option>
|
||||
@@ -35,21 +37,33 @@ function updateStyle(key: string, value: unknown) {
|
||||
</div>
|
||||
<div class="prop-row" data-tip="Yazi tipi boyutu (point)">
|
||||
<label class="prop-label">Boyut (pt)</label>
|
||||
<input class="prop-input" type="number" step="1" min="1"
|
||||
<input
|
||||
class="prop-input"
|
||||
type="number"
|
||||
step="1"
|
||||
min="1"
|
||||
:value="(element.style as TextStyle).fontSize ?? 10"
|
||||
@input="(e) => updateStyle('fontSize', parseFloat((e.target as HTMLInputElement).value) || 10)" />
|
||||
@input="
|
||||
(e) => updateStyle('fontSize', parseFloat((e.target as HTMLInputElement).value) || 10)
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
<div class="prop-row" data-tip="Metin rengi">
|
||||
<label class="prop-label">Renk</label>
|
||||
<input class="prop-input prop-color" type="color"
|
||||
<input
|
||||
class="prop-input prop-color"
|
||||
type="color"
|
||||
:value="(element.style as TextStyle).color ?? '#666666'"
|
||||
@input="(e) => updateStyle('color', (e.target as HTMLInputElement).value)" />
|
||||
@input="(e) => updateStyle('color', (e.target as HTMLInputElement).value)"
|
||||
/>
|
||||
</div>
|
||||
<div class="prop-row" data-tip="Metnin yatay hizalamasi">
|
||||
<label class="prop-label">Hizalama</label>
|
||||
<select class="prop-input prop-select"
|
||||
<select
|
||||
class="prop-input prop-select"
|
||||
:value="(element.style as TextStyle).align ?? 'left'"
|
||||
@change="(e) => updateStyle('align', (e.target as HTMLSelectElement).value)">
|
||||
@change="(e) => updateStyle('align', (e.target as HTMLSelectElement).value)"
|
||||
>
|
||||
<option value="left">Sol</option>
|
||||
<option value="center">Orta</option>
|
||||
<option value="right">Sag</option>
|
||||
|
||||
@@ -40,7 +40,9 @@ function setMode(mode: 'static' | 'dynamic') {
|
||||
update({ binding: undefined } as Partial<TemplateElement>)
|
||||
} else {
|
||||
// Dinamik moda geç — ilk uygun alanı seç veya boş bırak
|
||||
const imageFields = schemaStore.scalarFields.filter(f => f.format === 'image' || f.type === 'string')
|
||||
const imageFields = schemaStore.scalarFields.filter(
|
||||
(f) => f.format === 'image' || f.type === 'string',
|
||||
)
|
||||
const path = imageFields.length > 0 ? imageFields[0].path : ''
|
||||
update({ src: undefined, binding: { type: 'scalar', path } } as Partial<TemplateElement>)
|
||||
}
|
||||
@@ -52,7 +54,7 @@ function setBindingPath(path: string) {
|
||||
|
||||
/** Schema'dan görsel olabilecek alanlar (format: image veya string) */
|
||||
const imageScalarFields = computed(() => {
|
||||
return schemaStore.scalarFields.filter(f => f.format === 'image' || f.type === 'string')
|
||||
return schemaStore.scalarFields.filter((f) => f.format === 'image' || f.type === 'string')
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -64,8 +66,20 @@ const imageScalarFields = computed(() => {
|
||||
<div class="prop-row" data-tip="Gorsel kaynagi: dosya veya veri alanından">
|
||||
<label class="prop-label">Mod</label>
|
||||
<div class="prop-toggle-group">
|
||||
<button class="prop-toggle-btn" :class="{ 'prop-toggle-btn--active': !isDynamic }" @click="setMode('static')">Statik</button>
|
||||
<button class="prop-toggle-btn" :class="{ 'prop-toggle-btn--active': isDynamic }" @click="setMode('dynamic')">Dinamik</button>
|
||||
<button
|
||||
class="prop-toggle-btn"
|
||||
:class="{ 'prop-toggle-btn--active': !isDynamic }"
|
||||
@click="setMode('static')"
|
||||
>
|
||||
Statik
|
||||
</button>
|
||||
<button
|
||||
class="prop-toggle-btn"
|
||||
:class="{ 'prop-toggle-btn--active': isDynamic }"
|
||||
@click="setMode('dynamic')"
|
||||
>
|
||||
Dinamik
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -84,7 +98,9 @@ const imageScalarFields = computed(() => {
|
||||
</div>
|
||||
<div v-if="element.src" class="prop-row" data-tip="Gorseli kaldirmak icin tiklayin">
|
||||
<label class="prop-label"></label>
|
||||
<button class="prop-clear" @click="update({ src: undefined } as any)">Gorseli kaldir</button>
|
||||
<button class="prop-clear" @click="update({ src: undefined } as any)">
|
||||
Gorseli kaldir
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -92,15 +108,15 @@ const imageScalarFields = computed(() => {
|
||||
<template v-else>
|
||||
<div class="prop-row" data-tip="Gorsel URL'sinin gelecegi veri alani">
|
||||
<label class="prop-label">Veri Alani</label>
|
||||
<select class="prop-input prop-select"
|
||||
<select
|
||||
class="prop-input prop-select"
|
||||
:value="element.binding?.path ?? ''"
|
||||
@change="(e) => setBindingPath((e.target as HTMLSelectElement).value)">
|
||||
@change="(e) => setBindingPath((e.target as HTMLSelectElement).value)"
|
||||
>
|
||||
<option value="" disabled>Secin...</option>
|
||||
<option
|
||||
v-for="field in imageScalarFields"
|
||||
:key="field.path"
|
||||
:value="field.path"
|
||||
>{{ field.title }} ({{ field.path }})</option>
|
||||
<option v-for="field in imageScalarFields" :key="field.path" :value="field.path">
|
||||
{{ field.title }} ({{ field.path }})
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
<div v-if="element.binding?.path" class="prop-row">
|
||||
@@ -112,9 +128,11 @@ const imageScalarFields = computed(() => {
|
||||
<!-- Sığdırma modu (ortak) -->
|
||||
<div class="prop-row" data-tip="Gorselin alana sigdirma modu">
|
||||
<label class="prop-label">Sigdirma</label>
|
||||
<select class="prop-input prop-select"
|
||||
<select
|
||||
class="prop-input prop-select"
|
||||
:value="element.style.objectFit ?? 'contain'"
|
||||
@change="(e) => updateStyle('objectFit', (e.target as HTMLSelectElement).value)">
|
||||
@change="(e) => updateStyle('objectFit', (e.target as HTMLSelectElement).value)"
|
||||
>
|
||||
<option value="contain">Sigdir</option>
|
||||
<option value="cover">Kap</option>
|
||||
<option value="stretch">Esnet</option>
|
||||
@@ -137,7 +155,9 @@ const imageScalarFields = computed(() => {
|
||||
color: #64748b;
|
||||
font-size: 11px;
|
||||
cursor: pointer;
|
||||
transition: background 0.1s, color 0.1s;
|
||||
transition:
|
||||
background 0.1s,
|
||||
color 0.1s;
|
||||
}
|
||||
|
||||
.prop-toggle-btn:first-child {
|
||||
|
||||
@@ -11,7 +11,9 @@ const editorStore = useEditorStore()
|
||||
function updateStyle(key: string, value: unknown) {
|
||||
const id = editorStore.selectedElementId
|
||||
if (!id) return
|
||||
templateStore.updateElement(id, { style: { ...props.element.style, [key]: value } } as Partial<TemplateElement>)
|
||||
templateStore.updateElement(id, {
|
||||
style: { ...props.element.style, [key]: value },
|
||||
} as Partial<TemplateElement>)
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -20,15 +22,25 @@ function updateStyle(key: string, value: unknown) {
|
||||
<div class="prop-section__title">Cizgi Stili</div>
|
||||
<div class="prop-row" data-tip="Cizgi kalinligi (mm)">
|
||||
<label class="prop-label">Kalinlik (mm)</label>
|
||||
<input class="prop-input" type="number" step="0.1" min="0.1"
|
||||
<input
|
||||
class="prop-input"
|
||||
type="number"
|
||||
step="0.1"
|
||||
min="0.1"
|
||||
:value="element.style.strokeWidth ?? 0.5"
|
||||
@input="(e) => updateStyle('strokeWidth', parseFloat((e.target as HTMLInputElement).value) || 0.5)" />
|
||||
@input="
|
||||
(e) => updateStyle('strokeWidth', parseFloat((e.target as HTMLInputElement).value) || 0.5)
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
<div class="prop-row" data-tip="Cizgi rengi">
|
||||
<label class="prop-label">Renk</label>
|
||||
<input class="prop-input prop-color" type="color"
|
||||
<input
|
||||
class="prop-input prop-color"
|
||||
type="color"
|
||||
:value="element.style.strokeColor ?? '#000000'"
|
||||
@input="(e) => updateStyle('strokeColor', (e.target as HTMLInputElement).value)" />
|
||||
@input="(e) => updateStyle('strokeColor', (e.target as HTMLInputElement).value)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -22,10 +22,42 @@ function onInput(side: 'top' | 'right' | 'bottom' | 'left', e: Event) {
|
||||
<div class="pb">
|
||||
<span class="pb__label">Padding</span>
|
||||
<div class="pb__box">
|
||||
<input class="pb__in pb__in--t" type="number" step="1" min="0" :value="props.top" @input="(e) => onInput('top', e)" data-tip="Ust bosluk (mm)" />
|
||||
<input class="pb__in pb__in--r" type="number" step="1" min="0" :value="props.right" @input="(e) => onInput('right', e)" data-tip="Sag bosluk (mm)" />
|
||||
<input class="pb__in pb__in--b" type="number" step="1" min="0" :value="props.bottom" @input="(e) => onInput('bottom', e)" data-tip="Alt bosluk (mm)" />
|
||||
<input class="pb__in pb__in--l" type="number" step="1" min="0" :value="props.left" @input="(e) => onInput('left', e)" data-tip="Sol bosluk (mm)" />
|
||||
<input
|
||||
class="pb__in pb__in--t"
|
||||
type="number"
|
||||
step="1"
|
||||
min="0"
|
||||
:value="props.top"
|
||||
@input="(e) => onInput('top', e)"
|
||||
data-tip="Ust bosluk (mm)"
|
||||
/>
|
||||
<input
|
||||
class="pb__in pb__in--r"
|
||||
type="number"
|
||||
step="1"
|
||||
min="0"
|
||||
:value="props.right"
|
||||
@input="(e) => onInput('right', e)"
|
||||
data-tip="Sag bosluk (mm)"
|
||||
/>
|
||||
<input
|
||||
class="pb__in pb__in--b"
|
||||
type="number"
|
||||
step="1"
|
||||
min="0"
|
||||
:value="props.bottom"
|
||||
@input="(e) => onInput('bottom', e)"
|
||||
data-tip="Alt bosluk (mm)"
|
||||
/>
|
||||
<input
|
||||
class="pb__in pb__in--l"
|
||||
type="number"
|
||||
step="1"
|
||||
min="0"
|
||||
:value="props.left"
|
||||
@input="(e) => onInput('left', e)"
|
||||
data-tip="Sol bosluk (mm)"
|
||||
/>
|
||||
<div class="pb__center" />
|
||||
</div>
|
||||
</div>
|
||||
@@ -87,11 +119,32 @@ function onInput(side: 'top' | 'right' | 'bottom' | 'left', e: Event) {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.pb__in:hover { background: #f1f5f9; }
|
||||
.pb__in:focus { background: white; box-shadow: 0 0 0 1px #93c5fd; }
|
||||
.pb__in:hover {
|
||||
background: #f1f5f9;
|
||||
}
|
||||
.pb__in:focus {
|
||||
background: white;
|
||||
box-shadow: 0 0 0 1px #93c5fd;
|
||||
}
|
||||
|
||||
.pb__in--t { top: 1px; left: 50%; transform: translateX(-50%); }
|
||||
.pb__in--b { bottom: 1px; left: 50%; transform: translateX(-50%); }
|
||||
.pb__in--l { left: 2px; top: 50%; transform: translateY(-50%); }
|
||||
.pb__in--r { right: 2px; top: 50%; transform: translateY(-50%); }
|
||||
.pb__in--t {
|
||||
top: 1px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
.pb__in--b {
|
||||
bottom: 1px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
.pb__in--l {
|
||||
left: 2px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
.pb__in--r {
|
||||
right: 2px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -24,9 +24,11 @@ function updateStyle(key: string, value: unknown) {
|
||||
<div class="prop-section__title">Sayfa Numarasi</div>
|
||||
<div class="prop-row" data-tip="Sayfa numarasi gosterim formati">
|
||||
<label class="prop-label">Format</label>
|
||||
<select class="prop-input prop-select"
|
||||
<select
|
||||
class="prop-input prop-select"
|
||||
:value="element.format ?? '{current} / {total}'"
|
||||
@change="(e) => update({ format: (e.target as HTMLSelectElement).value } as any)">
|
||||
@change="(e) => update({ format: (e.target as HTMLSelectElement).value } as any)"
|
||||
>
|
||||
<option value="{current} / {total}">1 / 5</option>
|
||||
<option value="{current}">1</option>
|
||||
<option value="Sayfa {current}">Sayfa 1</option>
|
||||
@@ -35,21 +37,33 @@ function updateStyle(key: string, value: unknown) {
|
||||
</div>
|
||||
<div class="prop-row" data-tip="Yazi tipi boyutu (point)">
|
||||
<label class="prop-label">Boyut (pt)</label>
|
||||
<input class="prop-input" type="number" step="1" min="1"
|
||||
<input
|
||||
class="prop-input"
|
||||
type="number"
|
||||
step="1"
|
||||
min="1"
|
||||
:value="(element.style as TextStyle).fontSize ?? 10"
|
||||
@input="(e) => updateStyle('fontSize', parseFloat((e.target as HTMLInputElement).value) || 10)" />
|
||||
@input="
|
||||
(e) => updateStyle('fontSize', parseFloat((e.target as HTMLInputElement).value) || 10)
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
<div class="prop-row" data-tip="Metin rengi">
|
||||
<label class="prop-label">Renk</label>
|
||||
<input class="prop-input prop-color" type="color"
|
||||
<input
|
||||
class="prop-input prop-color"
|
||||
type="color"
|
||||
:value="(element.style as TextStyle).color ?? '#666666'"
|
||||
@input="(e) => updateStyle('color', (e.target as HTMLInputElement).value)" />
|
||||
@input="(e) => updateStyle('color', (e.target as HTMLInputElement).value)"
|
||||
/>
|
||||
</div>
|
||||
<div class="prop-row" data-tip="Metnin yatay hizalamasi">
|
||||
<label class="prop-label">Hizalama</label>
|
||||
<select class="prop-input prop-select"
|
||||
<select
|
||||
class="prop-input prop-select"
|
||||
:value="(element.style as TextStyle).align ?? 'center'"
|
||||
@change="(e) => updateStyle('align', (e.target as HTMLSelectElement).value)">
|
||||
@change="(e) => updateStyle('align', (e.target as HTMLSelectElement).value)"
|
||||
>
|
||||
<option value="left">Sol</option>
|
||||
<option value="center">Orta</option>
|
||||
<option value="right">Sag</option>
|
||||
|
||||
@@ -20,7 +20,11 @@ function togglePositioning() {
|
||||
<div class="prop-section__title">Pozisyon</div>
|
||||
<div class="prop-row" data-tip="Flow: otomatik dizilim, Absolute: sabit konum">
|
||||
<label class="prop-label">Mod</label>
|
||||
<select class="prop-input prop-select" :value="element.position.type" @change="togglePositioning">
|
||||
<select
|
||||
class="prop-input prop-select"
|
||||
:value="element.position.type"
|
||||
@change="togglePositioning"
|
||||
>
|
||||
<option value="flow">Flow</option>
|
||||
<option value="absolute">Absolute</option>
|
||||
</select>
|
||||
@@ -28,15 +32,37 @@ function togglePositioning() {
|
||||
<template v-if="element.position.type === 'absolute'">
|
||||
<div class="prop-row" data-tip="Yatay pozisyon — parent sol kenardan uzaklik (mm)">
|
||||
<label class="prop-label">X (mm)</label>
|
||||
<input class="prop-input" type="number" step="0.5"
|
||||
<input
|
||||
class="prop-input"
|
||||
type="number"
|
||||
step="0.5"
|
||||
:value="element.position.x"
|
||||
@input="(e) => templateStore.updateElementPosition(element.id, { type: 'absolute', x: parseFloat((e.target as HTMLInputElement).value) || 0, y: (element.position as any).y ?? 0 })" />
|
||||
@input="
|
||||
(e) =>
|
||||
templateStore.updateElementPosition(element.id, {
|
||||
type: 'absolute',
|
||||
x: parseFloat((e.target as HTMLInputElement).value) || 0,
|
||||
y: (element.position as any).y ?? 0,
|
||||
})
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
<div class="prop-row" data-tip="Dikey pozisyon — parent ust kenardan uzaklik (mm)">
|
||||
<label class="prop-label">Y (mm)</label>
|
||||
<input class="prop-input" type="number" step="0.5"
|
||||
<input
|
||||
class="prop-input"
|
||||
type="number"
|
||||
step="0.5"
|
||||
:value="element.position.y"
|
||||
@input="(e) => templateStore.updateElementPosition(element.id, { type: 'absolute', x: (element.position as any).x ?? 0, y: parseFloat((e.target as HTMLInputElement).value) || 0 })" />
|
||||
@input="
|
||||
(e) =>
|
||||
templateStore.updateElementPosition(element.id, {
|
||||
type: 'absolute',
|
||||
x: (element.position as any).x ?? 0,
|
||||
y: parseFloat((e.target as HTMLInputElement).value) || 0,
|
||||
})
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
@@ -5,7 +5,12 @@ import { useEditorStore } from '../../stores/editor'
|
||||
import { useSchemaStore } from '../../stores/schema'
|
||||
import { sz } from '../../core/types'
|
||||
import { schemaFormatToFormatType, defaultAlignForSchema } from '../../core/schema-parser'
|
||||
import type { RepeatingTableElement, TableColumn, FormatType, TemplateElement } from '../../core/types'
|
||||
import type {
|
||||
RepeatingTableElement,
|
||||
TableColumn,
|
||||
FormatType,
|
||||
TemplateElement,
|
||||
} from '../../core/types'
|
||||
import '../../styles/properties.css'
|
||||
|
||||
const props = defineProps<{ element: RepeatingTableElement }>()
|
||||
@@ -27,7 +32,7 @@ function nextColId() {
|
||||
function updateTableDataSource(path: string) {
|
||||
const itemFields = schemaStore.getArrayItemFields(path)
|
||||
if (itemFields.length > 0) {
|
||||
const columns: TableColumn[] = itemFields.map(field => ({
|
||||
const columns: TableColumn[] = itemFields.map((field) => ({
|
||||
id: nextColId(),
|
||||
field: field.key,
|
||||
title: field.title,
|
||||
@@ -51,7 +56,7 @@ function updateTableStyle(key: string, value: unknown) {
|
||||
}
|
||||
|
||||
function updateColumn(colId: string, updates: Partial<TableColumn>) {
|
||||
const columns = props.element.columns.map(c => c.id === colId ? { ...c, ...updates } : c)
|
||||
const columns = props.element.columns.map((c) => (c.id === colId ? { ...c, ...updates } : c))
|
||||
update({ columns } as Partial<TemplateElement>)
|
||||
}
|
||||
|
||||
@@ -67,12 +72,14 @@ function addColumn() {
|
||||
}
|
||||
|
||||
function removeColumn(colId: string) {
|
||||
update({ columns: props.element.columns.filter(c => c.id !== colId) } as Partial<TemplateElement>)
|
||||
update({
|
||||
columns: props.element.columns.filter((c) => c.id !== colId),
|
||||
} as Partial<TemplateElement>)
|
||||
}
|
||||
|
||||
function moveColumn(colId: string, direction: -1 | 1) {
|
||||
const cols = [...props.element.columns]
|
||||
const idx = cols.findIndex(c => c.id === colId)
|
||||
const idx = cols.findIndex((c) => c.id === colId)
|
||||
const newIdx = idx + direction
|
||||
if (newIdx < 0 || newIdx >= cols.length) return
|
||||
;[cols[idx], cols[newIdx]] = [cols[newIdx], cols[idx]]
|
||||
@@ -90,15 +97,15 @@ const tableItemFields = computed(() => {
|
||||
<div class="prop-section__title">Veri Kaynagi</div>
|
||||
<div class="prop-row" data-tip="Tablonun baglanacagi array veri kaynagi">
|
||||
<label class="prop-label">Kaynak</label>
|
||||
<select class="prop-input prop-select"
|
||||
<select
|
||||
class="prop-input prop-select"
|
||||
:value="element.dataSource.path"
|
||||
@change="(e) => updateTableDataSource((e.target as HTMLSelectElement).value)">
|
||||
@change="(e) => updateTableDataSource((e.target as HTMLSelectElement).value)"
|
||||
>
|
||||
<option value="" disabled>Secin...</option>
|
||||
<option
|
||||
v-for="arr in schemaStore.arrayFields"
|
||||
:key="arr.path"
|
||||
:value="arr.path"
|
||||
>{{ arr.title }} ({{ arr.path }})</option>
|
||||
<option v-for="arr in schemaStore.arrayFields" :key="arr.path" :value="arr.path">
|
||||
{{ arr.title }} ({{ arr.path }})
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
@@ -109,26 +116,41 @@ const tableItemFields = computed(() => {
|
||||
Sutunlar
|
||||
<button class="prop-add-btn" @click="addColumn">+</button>
|
||||
</div>
|
||||
<div
|
||||
v-for="col in element.columns"
|
||||
:key="col.id"
|
||||
class="tbl-col"
|
||||
>
|
||||
<div v-for="col in element.columns" :key="col.id" class="tbl-col">
|
||||
<!-- Row 1: title + actions -->
|
||||
<div class="tbl-col__head">
|
||||
<input class="tbl-col__title" type="text" :value="col.title"
|
||||
<input
|
||||
class="tbl-col__title"
|
||||
type="text"
|
||||
:value="col.title"
|
||||
@change="(e) => updateColumn(col.id, { title: (e.target as HTMLInputElement).value })"
|
||||
:placeholder="col.field"
|
||||
data-tip="Sutun basligi" />
|
||||
data-tip="Sutun basligi"
|
||||
/>
|
||||
<div class="tbl-col__actions">
|
||||
<button class="tbl-col__act" @click="moveColumn(col.id, -1)" data-tip="Yukari tasi">
|
||||
<svg width="10" height="10" viewBox="0 0 10 10"><path d="M5 2L2 6h6L5 2z" fill="currentColor"/></svg>
|
||||
<svg width="10" height="10" viewBox="0 0 10 10">
|
||||
<path d="M5 2L2 6h6L5 2z" fill="currentColor" />
|
||||
</svg>
|
||||
</button>
|
||||
<button class="tbl-col__act" @click="moveColumn(col.id, 1)" data-tip="Asagi tasi">
|
||||
<svg width="10" height="10" viewBox="0 0 10 10"><path d="M5 8L2 4h6L5 8z" fill="currentColor"/></svg>
|
||||
<svg width="10" height="10" viewBox="0 0 10 10">
|
||||
<path d="M5 8L2 4h6L5 8z" fill="currentColor" />
|
||||
</svg>
|
||||
</button>
|
||||
<button class="tbl-col__act tbl-col__act--del" @click="removeColumn(col.id)" data-tip="Sutunu sil">
|
||||
<svg width="10" height="10" viewBox="0 0 10 10"><path d="M2 2l6 6M8 2l-6 6" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/></svg>
|
||||
<button
|
||||
class="tbl-col__act tbl-col__act--del"
|
||||
@click="removeColumn(col.id)"
|
||||
data-tip="Sutunu sil"
|
||||
>
|
||||
<svg width="10" height="10" viewBox="0 0 10 10">
|
||||
<path
|
||||
d="M2 2l6 6M8 2l-6 6"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.5"
|
||||
stroke-linecap="round"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -136,10 +158,15 @@ const tableItemFields = computed(() => {
|
||||
<!-- Row 2: field + align + format + width compact -->
|
||||
<div class="tbl-col__controls">
|
||||
<!-- Field -->
|
||||
<select v-if="tableItemFields.length > 0" class="tbl-col__field" :value="col.field" data-tip="Veri alani"
|
||||
@change="(e) => {
|
||||
<select
|
||||
v-if="tableItemFields.length > 0"
|
||||
class="tbl-col__field"
|
||||
:value="col.field"
|
||||
data-tip="Veri alani"
|
||||
@change="
|
||||
(e) => {
|
||||
const field = (e.target as HTMLSelectElement).value
|
||||
const node = tableItemFields.find(f => f.key === field)
|
||||
const node = tableItemFields.find((f) => f.key === field)
|
||||
if (node) {
|
||||
updateColumn(col.id, {
|
||||
field,
|
||||
@@ -150,23 +177,129 @@ const tableItemFields = computed(() => {
|
||||
} else {
|
||||
updateColumn(col.id, { field })
|
||||
}
|
||||
}">
|
||||
}
|
||||
"
|
||||
>
|
||||
<option v-for="f in tableItemFields" :key="f.key" :value="f.key">{{ f.key }}</option>
|
||||
</select>
|
||||
<input v-else class="tbl-col__field" type="text" :value="col.field"
|
||||
<input
|
||||
v-else
|
||||
class="tbl-col__field"
|
||||
type="text"
|
||||
:value="col.field"
|
||||
@change="(e) => updateColumn(col.id, { field: (e.target as HTMLInputElement).value })"
|
||||
data-tip="Veri alani" />
|
||||
data-tip="Veri alani"
|
||||
/>
|
||||
|
||||
<!-- Alignment icons -->
|
||||
<div class="tbl-col__align">
|
||||
<button class="tbl-col__align-btn" :class="{ 'tbl-col__align-btn--on': col.align === 'left' }" @click="updateColumn(col.id, { align: 'left' })" data-tip="Sola hizala">
|
||||
<svg width="12" height="12" viewBox="0 0 12 12"><line x1="1" y1="3" x2="11" y2="3" stroke="currentColor" stroke-width="1.3" stroke-linecap="round"/><line x1="1" y1="6" x2="8" y2="6" stroke="currentColor" stroke-width="1.3" stroke-linecap="round"/><line x1="1" y1="9" x2="10" y2="9" stroke="currentColor" stroke-width="1.3" stroke-linecap="round"/></svg>
|
||||
<button
|
||||
class="tbl-col__align-btn"
|
||||
:class="{ 'tbl-col__align-btn--on': col.align === 'left' }"
|
||||
@click="updateColumn(col.id, { align: 'left' })"
|
||||
data-tip="Sola hizala"
|
||||
>
|
||||
<svg width="12" height="12" viewBox="0 0 12 12">
|
||||
<line
|
||||
x1="1"
|
||||
y1="3"
|
||||
x2="11"
|
||||
y2="3"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.3"
|
||||
stroke-linecap="round"
|
||||
/>
|
||||
<line
|
||||
x1="1"
|
||||
y1="6"
|
||||
x2="8"
|
||||
y2="6"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.3"
|
||||
stroke-linecap="round"
|
||||
/>
|
||||
<line
|
||||
x1="1"
|
||||
y1="9"
|
||||
x2="10"
|
||||
y2="9"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.3"
|
||||
stroke-linecap="round"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
<button class="tbl-col__align-btn" :class="{ 'tbl-col__align-btn--on': col.align === 'center' }" @click="updateColumn(col.id, { align: 'center' })" data-tip="Ortala">
|
||||
<svg width="12" height="12" viewBox="0 0 12 12"><line x1="1" y1="3" x2="11" y2="3" stroke="currentColor" stroke-width="1.3" stroke-linecap="round"/><line x1="2.5" y1="6" x2="9.5" y2="6" stroke="currentColor" stroke-width="1.3" stroke-linecap="round"/><line x1="1.5" y1="9" x2="10.5" y2="9" stroke="currentColor" stroke-width="1.3" stroke-linecap="round"/></svg>
|
||||
<button
|
||||
class="tbl-col__align-btn"
|
||||
:class="{ 'tbl-col__align-btn--on': col.align === 'center' }"
|
||||
@click="updateColumn(col.id, { align: 'center' })"
|
||||
data-tip="Ortala"
|
||||
>
|
||||
<svg width="12" height="12" viewBox="0 0 12 12">
|
||||
<line
|
||||
x1="1"
|
||||
y1="3"
|
||||
x2="11"
|
||||
y2="3"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.3"
|
||||
stroke-linecap="round"
|
||||
/>
|
||||
<line
|
||||
x1="2.5"
|
||||
y1="6"
|
||||
x2="9.5"
|
||||
y2="6"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.3"
|
||||
stroke-linecap="round"
|
||||
/>
|
||||
<line
|
||||
x1="1.5"
|
||||
y1="9"
|
||||
x2="10.5"
|
||||
y2="9"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.3"
|
||||
stroke-linecap="round"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
<button class="tbl-col__align-btn" :class="{ 'tbl-col__align-btn--on': col.align === 'right' }" @click="updateColumn(col.id, { align: 'right' })" data-tip="Saga hizala">
|
||||
<svg width="12" height="12" viewBox="0 0 12 12"><line x1="1" y1="3" x2="11" y2="3" stroke="currentColor" stroke-width="1.3" stroke-linecap="round"/><line x1="4" y1="6" x2="11" y2="6" stroke="currentColor" stroke-width="1.3" stroke-linecap="round"/><line x1="2" y1="9" x2="11" y2="9" stroke="currentColor" stroke-width="1.3" stroke-linecap="round"/></svg>
|
||||
<button
|
||||
class="tbl-col__align-btn"
|
||||
:class="{ 'tbl-col__align-btn--on': col.align === 'right' }"
|
||||
@click="updateColumn(col.id, { align: 'right' })"
|
||||
data-tip="Saga hizala"
|
||||
>
|
||||
<svg width="12" height="12" viewBox="0 0 12 12">
|
||||
<line
|
||||
x1="1"
|
||||
y1="3"
|
||||
x2="11"
|
||||
y2="3"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.3"
|
||||
stroke-linecap="round"
|
||||
/>
|
||||
<line
|
||||
x1="4"
|
||||
y1="6"
|
||||
x2="11"
|
||||
y2="6"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.3"
|
||||
stroke-linecap="round"
|
||||
/>
|
||||
<line
|
||||
x1="2"
|
||||
y1="9"
|
||||
x2="11"
|
||||
y2="9"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.3"
|
||||
stroke-linecap="round"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -174,8 +307,18 @@ const tableItemFields = computed(() => {
|
||||
<!-- Row 3: format + width -->
|
||||
<div class="tbl-col__extra" data-tip="Veri gosterim formati">
|
||||
<label class="tbl-col__elabel">Format</label>
|
||||
<select class="tbl-col__fmt" :value="col.format ?? ''"
|
||||
@change="(e) => updateColumn(col.id, { format: ((e.target as HTMLSelectElement).value || undefined) as FormatType | undefined })">
|
||||
<select
|
||||
class="tbl-col__fmt"
|
||||
:value="col.format ?? ''"
|
||||
@change="
|
||||
(e) =>
|
||||
updateColumn(col.id, {
|
||||
format: ((e.target as HTMLSelectElement).value || undefined) as
|
||||
| FormatType
|
||||
| undefined,
|
||||
})
|
||||
"
|
||||
>
|
||||
<option value="">Yok</option>
|
||||
<option value="currency">Para birimi</option>
|
||||
<option value="number">Sayi</option>
|
||||
@@ -185,22 +328,45 @@ const tableItemFields = computed(() => {
|
||||
</div>
|
||||
<div class="tbl-col__extra" data-tip="Sutun genislik modu">
|
||||
<label class="tbl-col__elabel">Genislik</label>
|
||||
<select class="tbl-col__wtype" :value="col.width.type"
|
||||
@change="(e) => {
|
||||
<select
|
||||
class="tbl-col__wtype"
|
||||
:value="col.width.type"
|
||||
@change="
|
||||
(e) => {
|
||||
const t = (e.target as HTMLSelectElement).value
|
||||
if (t === 'auto') updateColumn(col.id, { width: { type: 'auto' } })
|
||||
else if (t === 'fr') updateColumn(col.id, { width: { type: 'fr', value: 1 } })
|
||||
else updateColumn(col.id, { width: { type: 'fixed', value: 30 } })
|
||||
}">
|
||||
}
|
||||
"
|
||||
>
|
||||
<option value="auto">Otomatik</option>
|
||||
<option value="fixed">Sabit (mm)</option>
|
||||
<option value="fr">Oran (fr)</option>
|
||||
</select>
|
||||
<span v-if="col.width.type === 'fixed' || col.width.type === 'fr'" class="ts-tip-wrap" :data-tip="col.width.type === 'fixed' ? 'Sabit genislik (mm)' : 'Oran degeri (fr)'">
|
||||
<input class="tbl-col__wval" type="number" step="1"
|
||||
<span
|
||||
v-if="col.width.type === 'fixed' || col.width.type === 'fr'"
|
||||
class="ts-tip-wrap"
|
||||
:data-tip="col.width.type === 'fixed' ? 'Sabit genislik (mm)' : 'Oran degeri (fr)'"
|
||||
>
|
||||
<input
|
||||
class="tbl-col__wval"
|
||||
type="number"
|
||||
step="1"
|
||||
:min="col.width.type === 'fixed' ? 5 : 1"
|
||||
:value="(col.width as any).value"
|
||||
@change="(e) => updateColumn(col.id, { width: { type: col.width.type, value: parseFloat((e.target as HTMLInputElement).value) || (col.width.type === 'fixed' ? 30 : 1) } as any })" />
|
||||
@change="
|
||||
(e) =>
|
||||
updateColumn(col.id, {
|
||||
width: {
|
||||
type: col.width.type,
|
||||
value:
|
||||
parseFloat((e.target as HTMLInputElement).value) ||
|
||||
(col.width.type === 'fixed' ? 30 : 1),
|
||||
} as any,
|
||||
})
|
||||
"
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -216,15 +382,36 @@ const tableItemFields = computed(() => {
|
||||
<div class="ts-val ts-val--pair">
|
||||
<span class="ts-sep">Icerik</span>
|
||||
<span class="ts-tip-wrap" data-tip="Icerik yazi boyutu (pt)">
|
||||
<input class="ts-num" type="number" step="1" min="6" max="99"
|
||||
<input
|
||||
class="ts-num"
|
||||
type="number"
|
||||
step="1"
|
||||
min="6"
|
||||
max="99"
|
||||
:value="element.style.fontSize ?? 10"
|
||||
@input="(e) => updateTableStyle('fontSize', parseFloat((e.target as HTMLInputElement).value) || 10)" />
|
||||
@input="
|
||||
(e) =>
|
||||
updateTableStyle('fontSize', parseFloat((e.target as HTMLInputElement).value) || 10)
|
||||
"
|
||||
/>
|
||||
</span>
|
||||
<span class="ts-sep">Header</span>
|
||||
<span class="ts-tip-wrap" data-tip="Header yazi boyutu (pt)">
|
||||
<input class="ts-num" type="number" step="1" min="6" max="99"
|
||||
<input
|
||||
class="ts-num"
|
||||
type="number"
|
||||
step="1"
|
||||
min="6"
|
||||
max="99"
|
||||
:value="element.style.headerFontSize ?? element.style.fontSize ?? 10"
|
||||
@input="(e) => updateTableStyle('headerFontSize', parseFloat((e.target as HTMLInputElement).value) || 10)" />
|
||||
@input="
|
||||
(e) =>
|
||||
updateTableStyle(
|
||||
'headerFontSize',
|
||||
parseFloat((e.target as HTMLInputElement).value) || 10,
|
||||
)
|
||||
"
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@@ -232,23 +419,38 @@ const tableItemFields = computed(() => {
|
||||
<label class="ts-lbl" data-tip="Header, metin ve zebra satirlari renkleri">Renkler</label>
|
||||
<div class="ts-val ts-val--colors">
|
||||
<div class="ts-color-item" data-tip="Header arkaplan rengi">
|
||||
<input class="ts-swatch" type="color"
|
||||
<input
|
||||
class="ts-swatch"
|
||||
type="color"
|
||||
:value="element.style.headerBg ?? '#f0f0f0'"
|
||||
@input="(e) => updateTableStyle('headerBg', (e.target as HTMLInputElement).value)" />
|
||||
@input="(e) => updateTableStyle('headerBg', (e.target as HTMLInputElement).value)"
|
||||
/>
|
||||
<span class="ts-clbl">Arkaplan</span>
|
||||
</div>
|
||||
<div class="ts-color-item" data-tip="Header metin rengi">
|
||||
<input class="ts-swatch" type="color"
|
||||
<input
|
||||
class="ts-swatch"
|
||||
type="color"
|
||||
:value="element.style.headerColor ?? '#000000'"
|
||||
@input="(e) => updateTableStyle('headerColor', (e.target as HTMLInputElement).value)" />
|
||||
@input="(e) => updateTableStyle('headerColor', (e.target as HTMLInputElement).value)"
|
||||
/>
|
||||
<span class="ts-clbl">Metin</span>
|
||||
</div>
|
||||
<div class="ts-color-item" data-tip="Zebra satir rengi — tek satirlar">
|
||||
<div class="ts-swatch-wrap">
|
||||
<input class="ts-swatch" type="color"
|
||||
<input
|
||||
class="ts-swatch"
|
||||
type="color"
|
||||
:value="element.style.zebraOdd ?? '#fafafa'"
|
||||
@input="(e) => updateTableStyle('zebraOdd', (e.target as HTMLInputElement).value)" />
|
||||
<button v-if="element.style.zebraOdd" class="ts-swatch-clr" @click="updateTableStyle('zebraOdd', undefined)">×</button>
|
||||
@input="(e) => updateTableStyle('zebraOdd', (e.target as HTMLInputElement).value)"
|
||||
/>
|
||||
<button
|
||||
v-if="element.style.zebraOdd"
|
||||
class="ts-swatch-clr"
|
||||
@click="updateTableStyle('zebraOdd', undefined)"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
<span class="ts-clbl">Zebra</span>
|
||||
</div>
|
||||
@@ -258,15 +460,36 @@ const tableItemFields = computed(() => {
|
||||
<label class="ts-lbl" data-tip="Tablo kenarlik rengi ve kalinligi">Kenarlik</label>
|
||||
<div class="ts-val ts-val--pair">
|
||||
<div class="ts-swatch-wrap" data-tip="Kenarlik rengi">
|
||||
<input class="ts-swatch" type="color"
|
||||
<input
|
||||
class="ts-swatch"
|
||||
type="color"
|
||||
:value="element.style.borderColor ?? '#cccccc'"
|
||||
@input="(e) => updateTableStyle('borderColor', (e.target as HTMLInputElement).value)" />
|
||||
<button v-if="element.style.borderColor" class="ts-swatch-clr" @click="updateTableStyle('borderColor', undefined)">×</button>
|
||||
@input="(e) => updateTableStyle('borderColor', (e.target as HTMLInputElement).value)"
|
||||
/>
|
||||
<button
|
||||
v-if="element.style.borderColor"
|
||||
class="ts-swatch-clr"
|
||||
@click="updateTableStyle('borderColor', undefined)"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
<span class="ts-tip-wrap" data-tip="Kenarlik kalinligi (mm)">
|
||||
<input class="ts-num" type="number" step="0.1" min="0" max="99"
|
||||
<input
|
||||
class="ts-num"
|
||||
type="number"
|
||||
step="0.1"
|
||||
min="0"
|
||||
max="99"
|
||||
:value="element.style.borderWidth ?? 0.5"
|
||||
@input="(e) => updateTableStyle('borderWidth', parseFloat((e.target as HTMLInputElement).value) || 0)" />
|
||||
@input="
|
||||
(e) =>
|
||||
updateTableStyle(
|
||||
'borderWidth',
|
||||
parseFloat((e.target as HTMLInputElement).value) || 0,
|
||||
)
|
||||
"
|
||||
/>
|
||||
</span>
|
||||
<span class="ts-unit">mm</span>
|
||||
</div>
|
||||
@@ -276,42 +499,96 @@ const tableItemFields = computed(() => {
|
||||
<div class="ts-val ts-val--pair">
|
||||
<span class="ts-pad-icon" data-tip="Yatay bosluk (mm)">↔</span>
|
||||
<span class="ts-tip-wrap" data-tip="Yatay ic bosluk (mm)">
|
||||
<input class="ts-num" type="number" step="0.5" min="0" max="99"
|
||||
<input
|
||||
class="ts-num"
|
||||
type="number"
|
||||
step="0.5"
|
||||
min="0"
|
||||
max="99"
|
||||
:value="element.style.cellPaddingH ?? 2"
|
||||
@input="(e) => updateTableStyle('cellPaddingH', parseFloat((e.target as HTMLInputElement).value) || 0)" />
|
||||
@input="
|
||||
(e) =>
|
||||
updateTableStyle(
|
||||
'cellPaddingH',
|
||||
parseFloat((e.target as HTMLInputElement).value) || 0,
|
||||
)
|
||||
"
|
||||
/>
|
||||
</span>
|
||||
<span class="ts-pad-icon" data-tip="Dikey bosluk (mm)">↕</span>
|
||||
<span class="ts-tip-wrap" data-tip="Dikey ic bosluk (mm)">
|
||||
<input class="ts-num" type="number" step="0.5" min="0" max="99"
|
||||
<input
|
||||
class="ts-num"
|
||||
type="number"
|
||||
step="0.5"
|
||||
min="0"
|
||||
max="99"
|
||||
:value="element.style.cellPaddingV ?? 1"
|
||||
@input="(e) => updateTableStyle('cellPaddingV', parseFloat((e.target as HTMLInputElement).value) || 0)" />
|
||||
@input="
|
||||
(e) =>
|
||||
updateTableStyle(
|
||||
'cellPaddingV',
|
||||
parseFloat((e.target as HTMLInputElement).value) || 0,
|
||||
)
|
||||
"
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- Header padding -->
|
||||
<label class="ts-lbl" data-tip="Header hucre bosluklari — yatay ve dikey (mm)">Header bosluk</label>
|
||||
<label class="ts-lbl" data-tip="Header hucre bosluklari — yatay ve dikey (mm)"
|
||||
>Header bosluk</label
|
||||
>
|
||||
<div class="ts-val ts-val--pair">
|
||||
<span class="ts-pad-icon" data-tip="Yatay bosluk (mm)">↔</span>
|
||||
<span class="ts-tip-wrap" data-tip="Header yatay bosluk (mm)">
|
||||
<input class="ts-num" type="number" step="0.5" min="0" max="99"
|
||||
<input
|
||||
class="ts-num"
|
||||
type="number"
|
||||
step="0.5"
|
||||
min="0"
|
||||
max="99"
|
||||
:value="element.style.headerPaddingH ?? element.style.cellPaddingH ?? 2"
|
||||
@input="(e) => updateTableStyle('headerPaddingH', parseFloat((e.target as HTMLInputElement).value) || 0)" />
|
||||
@input="
|
||||
(e) =>
|
||||
updateTableStyle(
|
||||
'headerPaddingH',
|
||||
parseFloat((e.target as HTMLInputElement).value) || 0,
|
||||
)
|
||||
"
|
||||
/>
|
||||
</span>
|
||||
<span class="ts-pad-icon" data-tip="Dikey bosluk (mm)">↕</span>
|
||||
<span class="ts-tip-wrap" data-tip="Header dikey bosluk (mm)">
|
||||
<input class="ts-num" type="number" step="0.5" min="0" max="99"
|
||||
<input
|
||||
class="ts-num"
|
||||
type="number"
|
||||
step="0.5"
|
||||
min="0"
|
||||
max="99"
|
||||
:value="element.style.headerPaddingV ?? element.style.cellPaddingV ?? 1"
|
||||
@input="(e) => updateTableStyle('headerPaddingV', parseFloat((e.target as HTMLInputElement).value) || 0)" />
|
||||
@input="
|
||||
(e) =>
|
||||
updateTableStyle(
|
||||
'headerPaddingV',
|
||||
parseFloat((e.target as HTMLInputElement).value) || 0,
|
||||
)
|
||||
"
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- Repeat header -->
|
||||
<label class="ts-lbl" data-tip="Cok sayfali tablolarda header'i her sayfada tekrarla">Header tekrarla</label>
|
||||
<label class="ts-lbl" data-tip="Cok sayfali tablolarda header'i her sayfada tekrarla"
|
||||
>Header tekrarla</label
|
||||
>
|
||||
<div class="ts-val">
|
||||
<label class="ts-toggle">
|
||||
<input type="checkbox"
|
||||
<input
|
||||
type="checkbox"
|
||||
:checked="element.repeatHeader !== false"
|
||||
@change="(e) => update({ repeatHeader: (e.target as HTMLInputElement).checked } as any)" />
|
||||
@change="(e) => update({ repeatHeader: (e.target as HTMLInputElement).checked } as any)"
|
||||
/>
|
||||
<span class="ts-toggle__track"></span>
|
||||
</label>
|
||||
</div>
|
||||
@@ -666,7 +943,7 @@ const tableItemFields = computed(() => {
|
||||
background: white;
|
||||
border-radius: 50%;
|
||||
transition: transform 0.15s;
|
||||
box-shadow: 0 1px 2px rgba(0,0,0,0.1);
|
||||
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.ts-toggle input:checked + .ts-toggle__track {
|
||||
|
||||
@@ -46,21 +46,33 @@ function removeSpan(index: number) {
|
||||
<div class="prop-section__title">Varsayilan Stil</div>
|
||||
<div class="prop-row" data-tip="Varsayilan yazi tipi boyutu (point)">
|
||||
<label class="prop-label">Boyut (pt)</label>
|
||||
<input class="prop-input" type="number" step="1" min="1"
|
||||
<input
|
||||
class="prop-input"
|
||||
type="number"
|
||||
step="1"
|
||||
min="1"
|
||||
:value="element.style.fontSize ?? 11"
|
||||
@input="(e) => updateStyle('fontSize', parseFloat((e.target as HTMLInputElement).value) || 11)" />
|
||||
@input="
|
||||
(e) => updateStyle('fontSize', parseFloat((e.target as HTMLInputElement).value) || 11)
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
<div class="prop-row" data-tip="Varsayilan metin rengi">
|
||||
<label class="prop-label">Renk</label>
|
||||
<input class="prop-input prop-color" type="color"
|
||||
<input
|
||||
class="prop-input prop-color"
|
||||
type="color"
|
||||
:value="element.style.color ?? '#000000'"
|
||||
@input="(e) => updateStyle('color', (e.target as HTMLInputElement).value)" />
|
||||
@input="(e) => updateStyle('color', (e.target as HTMLInputElement).value)"
|
||||
/>
|
||||
</div>
|
||||
<div class="prop-row" data-tip="Metnin yatay hizalamasi">
|
||||
<label class="prop-label">Hizalama</label>
|
||||
<select class="prop-input prop-select"
|
||||
<select
|
||||
class="prop-input prop-select"
|
||||
:value="element.style.align ?? 'left'"
|
||||
@change="(e) => updateStyle('align', (e.target as HTMLSelectElement).value)">
|
||||
@change="(e) => updateStyle('align', (e.target as HTMLSelectElement).value)"
|
||||
>
|
||||
<option value="left">Sol</option>
|
||||
<option value="center">Orta</option>
|
||||
<option value="right">Sag</option>
|
||||
@@ -82,33 +94,49 @@ function removeSpan(index: number) {
|
||||
class="prop-span-card__remove"
|
||||
@click="removeSpan(idx)"
|
||||
title="Sil"
|
||||
>×</button>
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="prop-row" data-tip="Span metin icerigi">
|
||||
<label class="prop-label">Metin</label>
|
||||
<input class="prop-input" type="text"
|
||||
<input
|
||||
class="prop-input"
|
||||
type="text"
|
||||
:value="span.text ?? ''"
|
||||
@input="(e) => updateSpan(idx, { text: (e.target as HTMLInputElement).value })" />
|
||||
@input="(e) => updateSpan(idx, { text: (e.target as HTMLInputElement).value })"
|
||||
/>
|
||||
</div>
|
||||
<div class="prop-row" data-tip="Span yazi boyutu — bos birakilirsa varsayilan kullanilir">
|
||||
<label class="prop-label">Boyut</label>
|
||||
<input class="prop-input" type="number" step="1" min="1"
|
||||
<input
|
||||
class="prop-input"
|
||||
type="number"
|
||||
step="1"
|
||||
min="1"
|
||||
:value="(span.style as TextStyle).fontSize ?? ''"
|
||||
placeholder="varsayilan"
|
||||
@input="(e) => {
|
||||
@input="
|
||||
(e) => {
|
||||
const v = (e.target as HTMLInputElement).value
|
||||
updateSpanStyle(idx, 'fontSize', v ? parseFloat(v) : undefined)
|
||||
}" />
|
||||
}
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
<div class="prop-row" data-tip="Span yazi kalinligi">
|
||||
<label class="prop-label">Kalinlik</label>
|
||||
<select class="prop-input prop-select"
|
||||
<select
|
||||
class="prop-input prop-select"
|
||||
:value="(span.style as TextStyle).fontWeight ?? ''"
|
||||
@change="(e) => {
|
||||
@change="
|
||||
(e) => {
|
||||
const v = (e.target as HTMLSelectElement).value
|
||||
updateSpanStyle(idx, 'fontWeight', v || undefined)
|
||||
}">
|
||||
}
|
||||
"
|
||||
>
|
||||
<option value="">Varsayilan</option>
|
||||
<option value="normal">Normal</option>
|
||||
<option value="bold">Kalin</option>
|
||||
@@ -116,9 +144,12 @@ function removeSpan(index: number) {
|
||||
</div>
|
||||
<div class="prop-row" data-tip="Span metin rengi">
|
||||
<label class="prop-label">Renk</label>
|
||||
<input class="prop-input prop-color" type="color"
|
||||
<input
|
||||
class="prop-input prop-color"
|
||||
type="color"
|
||||
:value="(span.style as TextStyle).color ?? element.style.color ?? '#000000'"
|
||||
@input="(e) => updateSpanStyle(idx, 'color', (e.target as HTMLInputElement).value)" />
|
||||
@input="(e) => updateSpanStyle(idx, 'color', (e.target as HTMLInputElement).value)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -24,9 +24,11 @@ function updateStyle(key: string, value: unknown) {
|
||||
<div class="prop-section__title">Sekil</div>
|
||||
<div class="prop-row" data-tip="Sekil tipi">
|
||||
<label class="prop-label">Tip</label>
|
||||
<select class="prop-input prop-select"
|
||||
<select
|
||||
class="prop-input prop-select"
|
||||
:value="element.shapeType"
|
||||
@change="(e) => update({ shapeType: (e.target as HTMLSelectElement).value } as any)">
|
||||
@change="(e) => update({ shapeType: (e.target as HTMLSelectElement).value } as any)"
|
||||
>
|
||||
<option value="rectangle">Dikdortgen</option>
|
||||
<option value="rounded_rectangle">Yuvarlak Dikdortgen</option>
|
||||
<option value="ellipse">Elips</option>
|
||||
@@ -34,27 +36,51 @@ function updateStyle(key: string, value: unknown) {
|
||||
</div>
|
||||
<div class="prop-row" data-tip="Sekil arka plan rengi">
|
||||
<label class="prop-label">Arka Plan</label>
|
||||
<input class="prop-input prop-color" type="color"
|
||||
<input
|
||||
class="prop-input prop-color"
|
||||
type="color"
|
||||
:value="element.style.backgroundColor ?? '#f0f0f0'"
|
||||
@input="(e) => updateStyle('backgroundColor', (e.target as HTMLInputElement).value)" />
|
||||
@input="(e) => updateStyle('backgroundColor', (e.target as HTMLInputElement).value)"
|
||||
/>
|
||||
</div>
|
||||
<div class="prop-row" data-tip="Kenarlik cizgisi rengi">
|
||||
<label class="prop-label">Kenar Rengi</label>
|
||||
<input class="prop-input prop-color" type="color"
|
||||
<input
|
||||
class="prop-input prop-color"
|
||||
type="color"
|
||||
:value="element.style.borderColor ?? '#333333'"
|
||||
@input="(e) => updateStyle('borderColor', (e.target as HTMLInputElement).value)" />
|
||||
@input="(e) => updateStyle('borderColor', (e.target as HTMLInputElement).value)"
|
||||
/>
|
||||
</div>
|
||||
<div class="prop-row" data-tip="Kenarlik cizgi kalinligi (mm)">
|
||||
<label class="prop-label">Kenar Kalinligi</label>
|
||||
<input class="prop-input" type="number" step="0.25" min="0"
|
||||
<input
|
||||
class="prop-input"
|
||||
type="number"
|
||||
step="0.25"
|
||||
min="0"
|
||||
:value="element.style.borderWidth ?? 0.5"
|
||||
@input="(e) => updateStyle('borderWidth', parseFloat((e.target as HTMLInputElement).value) || 0)" />
|
||||
@input="
|
||||
(e) => updateStyle('borderWidth', parseFloat((e.target as HTMLInputElement).value) || 0)
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
<div v-if="element.shapeType === 'rounded_rectangle'" class="prop-row" data-tip="Kose yuvarlakligi (mm)">
|
||||
<div
|
||||
v-if="element.shapeType === 'rounded_rectangle'"
|
||||
class="prop-row"
|
||||
data-tip="Kose yuvarlakligi (mm)"
|
||||
>
|
||||
<label class="prop-label">Kose Yuvarlakligi</label>
|
||||
<input class="prop-input" type="number" step="0.5" min="0"
|
||||
<input
|
||||
class="prop-input"
|
||||
type="number"
|
||||
step="0.5"
|
||||
min="0"
|
||||
:value="element.style.borderRadius ?? 2"
|
||||
@input="(e) => updateStyle('borderRadius', parseFloat((e.target as HTMLInputElement).value) || 0)" />
|
||||
@input="
|
||||
(e) => updateStyle('borderRadius', parseFloat((e.target as HTMLInputElement).value) || 0)
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -16,52 +16,105 @@ function updateSize(axis: 'width' | 'height', sv: SizeValue) {
|
||||
<div class="prop-section__title">Boyut</div>
|
||||
<div class="prop-row" data-tip="Genislik boyutlandirma modu">
|
||||
<label class="prop-label">Genislik</label>
|
||||
<select class="prop-input prop-select"
|
||||
<select
|
||||
class="prop-input prop-select"
|
||||
:value="element.size.width.type"
|
||||
@change="(e) => {
|
||||
@change="
|
||||
(e) => {
|
||||
const t = (e.target as HTMLSelectElement).value
|
||||
if (t === 'auto') updateSize('width', { type: 'auto' })
|
||||
else if (t === 'fr') updateSize('width', { type: 'fr', value: 1 })
|
||||
else updateSize('width', { type: 'fixed', value: 50 })
|
||||
}">
|
||||
}
|
||||
"
|
||||
>
|
||||
<option value="auto">Otomatik</option>
|
||||
<option value="fixed">Sabit (mm)</option>
|
||||
<option value="fr">Oran (fr)</option>
|
||||
</select>
|
||||
</div>
|
||||
<div v-if="element.size.width.type === 'fixed'" class="prop-row" data-tip="Sabit genislik degeri (mm)">
|
||||
<div
|
||||
v-if="element.size.width.type === 'fixed'"
|
||||
class="prop-row"
|
||||
data-tip="Sabit genislik degeri (mm)"
|
||||
>
|
||||
<label class="prop-label">mm</label>
|
||||
<input class="prop-input" type="number" step="1" min="1"
|
||||
<input
|
||||
class="prop-input"
|
||||
type="number"
|
||||
step="1"
|
||||
min="1"
|
||||
:value="(element.size.width as any).value"
|
||||
@input="(e) => updateSize('width', { type: 'fixed', value: parseFloat((e.target as HTMLInputElement).value) || 10 })" />
|
||||
@input="
|
||||
(e) =>
|
||||
updateSize('width', {
|
||||
type: 'fixed',
|
||||
value: parseFloat((e.target as HTMLInputElement).value) || 10,
|
||||
})
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
<div v-if="element.size.width.type === 'fr'" class="prop-row" data-tip="Kalan alani oransal doldurma degeri">
|
||||
<div
|
||||
v-if="element.size.width.type === 'fr'"
|
||||
class="prop-row"
|
||||
data-tip="Kalan alani oransal doldurma degeri"
|
||||
>
|
||||
<label class="prop-label">fr</label>
|
||||
<input class="prop-input" type="number" step="1" min="1"
|
||||
<input
|
||||
class="prop-input"
|
||||
type="number"
|
||||
step="1"
|
||||
min="1"
|
||||
:value="(element.size.width as any).value"
|
||||
@input="(e) => updateSize('width', { type: 'fr', value: parseFloat((e.target as HTMLInputElement).value) || 1 })" />
|
||||
@input="
|
||||
(e) =>
|
||||
updateSize('width', {
|
||||
type: 'fr',
|
||||
value: parseFloat((e.target as HTMLInputElement).value) || 1,
|
||||
})
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="prop-row" data-tip="Yukseklik boyutlandirma modu">
|
||||
<label class="prop-label">Yukseklik</label>
|
||||
<select class="prop-input prop-select"
|
||||
<select
|
||||
class="prop-input prop-select"
|
||||
:value="element.size.height.type"
|
||||
@change="(e) => {
|
||||
@change="
|
||||
(e) => {
|
||||
const t = (e.target as HTMLSelectElement).value
|
||||
if (t === 'auto') updateSize('height', { type: 'auto' })
|
||||
else if (t === 'fr') updateSize('height', { type: 'fr', value: 1 })
|
||||
else updateSize('height', { type: 'fixed', value: 20 })
|
||||
}">
|
||||
}
|
||||
"
|
||||
>
|
||||
<option value="auto">Otomatik</option>
|
||||
<option value="fixed">Sabit (mm)</option>
|
||||
<option value="fr">Oran (fr)</option>
|
||||
</select>
|
||||
</div>
|
||||
<div v-if="element.size.height.type === 'fixed'" class="prop-row" data-tip="Sabit yukseklik degeri (mm)">
|
||||
<div
|
||||
v-if="element.size.height.type === 'fixed'"
|
||||
class="prop-row"
|
||||
data-tip="Sabit yukseklik degeri (mm)"
|
||||
>
|
||||
<label class="prop-label">mm</label>
|
||||
<input class="prop-input" type="number" step="1" min="1"
|
||||
<input
|
||||
class="prop-input"
|
||||
type="number"
|
||||
step="1"
|
||||
min="1"
|
||||
:value="(element.size.height as any).value"
|
||||
@input="(e) => updateSize('height', { type: 'fixed', value: parseFloat((e.target as HTMLInputElement).value) || 10 })" />
|
||||
@input="
|
||||
(e) =>
|
||||
updateSize('height', {
|
||||
type: 'fixed',
|
||||
value: parseFloat((e.target as HTMLInputElement).value) || 10,
|
||||
})
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -25,37 +25,54 @@ function updateStyle(key: string, value: unknown) {
|
||||
|
||||
<div v-if="element.type === 'static_text'" class="prop-row" data-tip="Sabit metin icerigi">
|
||||
<label class="prop-label">Metin</label>
|
||||
<input class="prop-input" type="text"
|
||||
<input
|
||||
class="prop-input"
|
||||
type="text"
|
||||
:value="(element as StaticTextElement).content"
|
||||
@input="(e) => update({ content: (e.target as HTMLInputElement).value } as any)" />
|
||||
@input="(e) => update({ content: (e.target as HTMLInputElement).value } as any)"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="prop-row" data-tip="Yazi tipi boyutu (point)">
|
||||
<label class="prop-label">Boyut (pt)</label>
|
||||
<input class="prop-input" type="number" step="1" min="1"
|
||||
<input
|
||||
class="prop-input"
|
||||
type="number"
|
||||
step="1"
|
||||
min="1"
|
||||
:value="(element.style as TextStyle).fontSize ?? 11"
|
||||
@input="(e) => updateStyle('fontSize', parseFloat((e.target as HTMLInputElement).value) || 11)" />
|
||||
@input="
|
||||
(e) => updateStyle('fontSize', parseFloat((e.target as HTMLInputElement).value) || 11)
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
<div class="prop-row" data-tip="Yazi tipi kalinligi">
|
||||
<label class="prop-label">Kalinlik</label>
|
||||
<select class="prop-input prop-select"
|
||||
<select
|
||||
class="prop-input prop-select"
|
||||
:value="(element.style as TextStyle).fontWeight ?? 'normal'"
|
||||
@change="(e) => updateStyle('fontWeight', (e.target as HTMLSelectElement).value)">
|
||||
@change="(e) => updateStyle('fontWeight', (e.target as HTMLSelectElement).value)"
|
||||
>
|
||||
<option value="normal">Normal</option>
|
||||
<option value="bold">Kalin</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="prop-row" data-tip="Metin rengi">
|
||||
<label class="prop-label">Renk</label>
|
||||
<input class="prop-input prop-color" type="color"
|
||||
<input
|
||||
class="prop-input prop-color"
|
||||
type="color"
|
||||
:value="(element.style as TextStyle).color ?? '#000000'"
|
||||
@input="(e) => updateStyle('color', (e.target as HTMLInputElement).value)" />
|
||||
@input="(e) => updateStyle('color', (e.target as HTMLInputElement).value)"
|
||||
/>
|
||||
</div>
|
||||
<div class="prop-row" data-tip="Metnin yatay hizalamasi">
|
||||
<label class="prop-label">Hizalama</label>
|
||||
<select class="prop-input prop-select"
|
||||
<select
|
||||
class="prop-input prop-select"
|
||||
:value="(element.style as TextStyle).align ?? 'left'"
|
||||
@change="(e) => updateStyle('align', (e.target as HTMLSelectElement).value)">
|
||||
@change="(e) => updateStyle('align', (e.target as HTMLSelectElement).value)"
|
||||
>
|
||||
<option value="left">Sol</option>
|
||||
<option value="center">Orta</option>
|
||||
<option value="right">Sag</option>
|
||||
|
||||
@@ -121,11 +121,20 @@ export function useLayoutEngine(
|
||||
|
||||
// --- Barcode üretimi (WASM üzerinden) ---
|
||||
let barcodeReqId = 0
|
||||
const barcodeCallbacks = new Map<number, (result: { width: number; height: number; rgba: ArrayBuffer } | null) => void>()
|
||||
const barcodeCallbacks = new Map<
|
||||
number,
|
||||
(result: { width: number; height: number; rgba: ArrayBuffer } | null) => void
|
||||
>()
|
||||
|
||||
function generateBarcode(format: string, value: string, width: number, height: number, includeText: boolean = false): Promise<{ width: number; height: number; rgba: ArrayBuffer } | null> {
|
||||
function generateBarcode(
|
||||
format: string,
|
||||
value: string,
|
||||
width: number,
|
||||
height: number,
|
||||
includeText: boolean = false,
|
||||
): Promise<{ width: number; height: number; rgba: ArrayBuffer } | null> {
|
||||
if (!worker) initWorker()
|
||||
return new Promise(resolve => {
|
||||
return new Promise((resolve) => {
|
||||
barcodeReqId++
|
||||
const id = barcodeReqId
|
||||
const timeout = setTimeout(() => {
|
||||
@@ -140,11 +149,17 @@ export function useLayoutEngine(
|
||||
})
|
||||
}
|
||||
|
||||
function handleBarcodeResponse(msg: Extract<WorkerResponse, { type: 'barcode-result' } | { type: 'barcode-error' }>) {
|
||||
function handleBarcodeResponse(
|
||||
msg: Extract<WorkerResponse, { type: 'barcode-result' } | { type: 'barcode-error' }>,
|
||||
) {
|
||||
const cb = barcodeCallbacks.get(msg.id)
|
||||
if (cb) {
|
||||
barcodeCallbacks.delete(msg.id)
|
||||
cb(msg.type === 'barcode-result' ? { width: msg.width, height: msg.height, rgba: msg.rgba } : null)
|
||||
cb(
|
||||
msg.type === 'barcode-result'
|
||||
? { width: msg.width, height: msg.height, rgba: msg.rgba }
|
||||
: null,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -27,7 +27,7 @@ export function useSnapGuides() {
|
||||
layoutMap: Record<string, ElementLayout>,
|
||||
excludeId: string,
|
||||
pageWidth: number,
|
||||
pageHeight: number
|
||||
pageHeight: number,
|
||||
) {
|
||||
const verticals: number[] = [0, pageWidth / 2, pageWidth] // page edges + center
|
||||
const horizontals: number[] = [0, pageHeight / 2, pageHeight]
|
||||
@@ -48,7 +48,7 @@ export function useSnapGuides() {
|
||||
proposedX_mm: number,
|
||||
proposedY_mm: number,
|
||||
width_mm: number,
|
||||
height_mm: number
|
||||
height_mm: number,
|
||||
): SnapResult {
|
||||
if (!cachedEdges) {
|
||||
return { snappedX_mm: proposedX_mm, snappedY_mm: proposedY_mm, guides: [] }
|
||||
@@ -132,13 +132,12 @@ export function useSnapGuides() {
|
||||
/** Calculate snap for resize edge */
|
||||
function calculateResizeSnap(
|
||||
edge: 'left' | 'right' | 'top' | 'bottom',
|
||||
proposedValue_mm: number
|
||||
proposedValue_mm: number,
|
||||
): number {
|
||||
if (!cachedEdges) return proposedValue_mm
|
||||
|
||||
const targets = (edge === 'left' || edge === 'right')
|
||||
? cachedEdges.verticals
|
||||
: cachedEdges.horizontals
|
||||
const targets =
|
||||
edge === 'left' || edge === 'right' ? cachedEdges.verticals : cachedEdges.horizontals
|
||||
|
||||
const guides: SnapGuide[] = []
|
||||
let snapped = proposedValue_mm
|
||||
@@ -154,7 +153,7 @@ export function useSnapGuides() {
|
||||
|
||||
if (snapped !== proposedValue_mm) {
|
||||
guides.push({
|
||||
type: (edge === 'left' || edge === 'right') ? 'vertical' : 'horizontal',
|
||||
type: edge === 'left' || edge === 'right' ? 'vertical' : 'horizontal',
|
||||
position_mm: snapped,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -29,7 +29,7 @@ export function useUndoRedo<T>(source: Ref<T>, maxHistory = 50) {
|
||||
redoStack.value = []
|
||||
}, 300)
|
||||
},
|
||||
{ deep: true }
|
||||
{ deep: true },
|
||||
)
|
||||
|
||||
function undo() {
|
||||
|
||||
@@ -148,7 +148,7 @@ describe('findScalarFields', () => {
|
||||
// firma.unvan, firma.vergiNo, fatura.no, fatura.tutar, fatura.tarih = 5
|
||||
expect(scalars).toHaveLength(5)
|
||||
|
||||
const paths = scalars.map(s => s.path)
|
||||
const paths = scalars.map((s) => s.path)
|
||||
expect(paths).toContain('firma.unvan')
|
||||
expect(paths).toContain('firma.vergiNo')
|
||||
expect(paths).toContain('fatura.no')
|
||||
@@ -159,7 +159,7 @@ describe('findScalarFields', () => {
|
||||
it('does not include object or array nodes', () => {
|
||||
const tree = parseSchema(testSchema)
|
||||
const scalars = findScalarFields(tree)
|
||||
const types = scalars.map(s => s.type)
|
||||
const types = scalars.map((s) => s.type)
|
||||
|
||||
expect(types).not.toContain('object')
|
||||
expect(types).not.toContain('array')
|
||||
@@ -195,17 +195,38 @@ describe('defaultAlignForSchema', () => {
|
||||
})
|
||||
|
||||
it('returns right for currency format', () => {
|
||||
const node: SchemaNode = { path: 'x', key: 'x', title: 'X', type: 'string', format: 'currency', children: [] }
|
||||
const node: SchemaNode = {
|
||||
path: 'x',
|
||||
key: 'x',
|
||||
title: 'X',
|
||||
type: 'string',
|
||||
format: 'currency',
|
||||
children: [],
|
||||
}
|
||||
expect(defaultAlignForSchema(node)).toBe('right')
|
||||
})
|
||||
|
||||
it('returns right for percentage format', () => {
|
||||
const node: SchemaNode = { path: 'x', key: 'x', title: 'X', type: 'string', format: 'percentage', children: [] }
|
||||
const node: SchemaNode = {
|
||||
path: 'x',
|
||||
key: 'x',
|
||||
title: 'X',
|
||||
type: 'string',
|
||||
format: 'percentage',
|
||||
children: [],
|
||||
}
|
||||
expect(defaultAlignForSchema(node)).toBe('right')
|
||||
})
|
||||
|
||||
it('returns center for date format', () => {
|
||||
const node: SchemaNode = { path: 'x', key: 'x', title: 'X', type: 'string', format: 'date', children: [] }
|
||||
const node: SchemaNode = {
|
||||
path: 'x',
|
||||
key: 'x',
|
||||
title: 'X',
|
||||
type: 'string',
|
||||
format: 'date',
|
||||
children: [],
|
||||
}
|
||||
expect(defaultAlignForSchema(node)).toBe('center')
|
||||
})
|
||||
|
||||
|
||||
@@ -57,10 +57,13 @@ function mockColumnValue(field: string, format: string | undefined, index: numbe
|
||||
const lower = field.toLowerCase()
|
||||
if (lower.includes('sira') || lower.includes('no') || lower === 'id') return index + 1
|
||||
if (lower.includes('miktar') || lower.includes('adet')) return [2, 1, 5][index % 3]
|
||||
if (lower.includes('fiyat') || lower.includes('tutar') || lower.includes('toplam')) return [1500, 2750, 500][index % 3]
|
||||
if (lower.includes('fiyat') || lower.includes('tutar') || lower.includes('toplam'))
|
||||
return [1500, 2750, 500][index % 3]
|
||||
if (lower.includes('birim')) return ['Adet', 'Saat', 'Adet'][index % 3]
|
||||
if (lower.includes('tarih') || lower.includes('date')) return ['2026-01-15', '2026-02-20', '2026-03-10'][index % 3]
|
||||
if (lower.includes('ad') || lower.includes('isim') || lower.includes('name')) return ['Kalem A', 'Kalem B', 'Kalem C'][index % 3]
|
||||
if (lower.includes('tarih') || lower.includes('date'))
|
||||
return ['2026-01-15', '2026-02-20', '2026-03-10'][index % 3]
|
||||
if (lower.includes('ad') || lower.includes('isim') || lower.includes('name'))
|
||||
return ['Kalem A', 'Kalem B', 'Kalem C'][index % 3]
|
||||
|
||||
return `Ornek ${index + 1}`
|
||||
}
|
||||
|
||||
@@ -73,7 +73,12 @@ export function findArrayFields(node: SchemaNode): SchemaNode[] {
|
||||
/** Schema ağacından tüm scalar alanları bulur (metin binding için) */
|
||||
export function findScalarFields(node: SchemaNode): SchemaNode[] {
|
||||
const result: SchemaNode[] = []
|
||||
if (node.type === 'string' || node.type === 'number' || node.type === 'integer' || node.type === 'boolean') {
|
||||
if (
|
||||
node.type === 'string' ||
|
||||
node.type === 'number' ||
|
||||
node.type === 'integer' ||
|
||||
node.type === 'boolean'
|
||||
) {
|
||||
result.push(node)
|
||||
}
|
||||
for (const child of node.children) {
|
||||
@@ -83,13 +88,19 @@ export function findScalarFields(node: SchemaNode): SchemaNode[] {
|
||||
}
|
||||
|
||||
/** Format tipinden FormatType'a dönüşüm */
|
||||
export function schemaFormatToFormatType(format?: string): 'currency' | 'date' | 'percentage' | 'number' | undefined {
|
||||
export function schemaFormatToFormatType(
|
||||
format?: string,
|
||||
): 'currency' | 'date' | 'percentage' | 'number' | undefined {
|
||||
if (!format) return undefined
|
||||
switch (format) {
|
||||
case 'currency': return 'currency'
|
||||
case 'date': return 'date'
|
||||
case 'percentage': return 'percentage'
|
||||
default: return undefined
|
||||
case 'currency':
|
||||
return 'currency'
|
||||
case 'date':
|
||||
return 'date'
|
||||
case 'percentage':
|
||||
return 'percentage'
|
||||
default:
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -293,7 +293,21 @@ export interface RepeatingTableElement extends BaseElement {
|
||||
repeatHeader?: boolean
|
||||
}
|
||||
|
||||
export type LeafElement = StaticTextElement | TextElement | LineElement | RepeatingTableElement | ImageElement | PageNumberElement | BarcodeElement | PageBreakElement | CurrentDateElement | ShapeElement | CheckboxElement | CalculatedTextElement | RichTextElement | ChartElement
|
||||
export type LeafElement =
|
||||
| StaticTextElement
|
||||
| TextElement
|
||||
| LineElement
|
||||
| RepeatingTableElement
|
||||
| ImageElement
|
||||
| PageNumberElement
|
||||
| BarcodeElement
|
||||
| PageBreakElement
|
||||
| CurrentDateElement
|
||||
| ShapeElement
|
||||
| CheckboxElement
|
||||
| CalculatedTextElement
|
||||
| RichTextElement
|
||||
| ChartElement
|
||||
export type TemplateElement = LeafElement | ContainerElement
|
||||
|
||||
// --- Template ---
|
||||
@@ -330,10 +344,7 @@ export function isLeaf(el: TemplateElement): el is LeafElement {
|
||||
}
|
||||
|
||||
/** Ağaçta bir element'i ID ile bulur */
|
||||
export function findElementById(
|
||||
root: ContainerElement,
|
||||
id: string
|
||||
): TemplateElement | undefined {
|
||||
export function findElementById(root: ContainerElement, id: string): TemplateElement | undefined {
|
||||
if (root.id === id) return root
|
||||
for (const child of root.children) {
|
||||
if (child.id === id) return child
|
||||
@@ -346,10 +357,7 @@ export function findElementById(
|
||||
}
|
||||
|
||||
/** Bir element'in parent container'ını bulur */
|
||||
export function findParent(
|
||||
root: ContainerElement,
|
||||
id: string
|
||||
): ContainerElement | undefined {
|
||||
export function findParent(root: ContainerElement, id: string): ContainerElement | undefined {
|
||||
for (const child of root.children) {
|
||||
if (child.id === id) return root
|
||||
if (isContainer(child)) {
|
||||
|
||||
@@ -58,23 +58,39 @@ let installed = false
|
||||
export function setupTooltips() {
|
||||
if (installed) return
|
||||
installed = true
|
||||
document.addEventListener('pointerenter', (e) => {
|
||||
document.addEventListener(
|
||||
'pointerenter',
|
||||
(e) => {
|
||||
const target = closest(e.target)
|
||||
if (target) show(target)
|
||||
}, true)
|
||||
},
|
||||
true,
|
||||
)
|
||||
|
||||
document.addEventListener('pointerleave', (e) => {
|
||||
document.addEventListener(
|
||||
'pointerleave',
|
||||
(e) => {
|
||||
const target = closest(e.target)
|
||||
if (target && target === currentTarget) hide()
|
||||
}, true)
|
||||
},
|
||||
true,
|
||||
)
|
||||
|
||||
document.addEventListener('focusin', (e) => {
|
||||
document.addEventListener(
|
||||
'focusin',
|
||||
(e) => {
|
||||
const target = closest(e.target)
|
||||
if (target) show(target)
|
||||
}, true)
|
||||
},
|
||||
true,
|
||||
)
|
||||
|
||||
document.addEventListener('focusout', (e) => {
|
||||
document.addEventListener(
|
||||
'focusout',
|
||||
(e) => {
|
||||
const target = closest(e.target)
|
||||
if (target && target === currentTarget) hide()
|
||||
}, true)
|
||||
},
|
||||
true,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -15,15 +15,18 @@ export interface DreportEditorConfig {
|
||||
apiBaseUrl?: string
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
schema: JsonSchema
|
||||
modelValue: Template
|
||||
data?: Record<string, unknown>
|
||||
config?: DreportEditorConfig
|
||||
handleErrors?: boolean
|
||||
}>(), {
|
||||
}>(),
|
||||
{
|
||||
handleErrors: true,
|
||||
})
|
||||
},
|
||||
)
|
||||
|
||||
const emit = defineEmits<{
|
||||
'update:modelValue': [value: Template]
|
||||
@@ -45,34 +48,55 @@ onMounted(() => {
|
||||
schemaStore.setSchema(props.schema)
|
||||
syncing = true
|
||||
templateStore.template = JSON.parse(JSON.stringify(props.modelValue))
|
||||
nextTick(() => { syncing = false })
|
||||
nextTick(() => {
|
||||
syncing = false
|
||||
})
|
||||
templateStore.setOverrideData(props.data ?? null)
|
||||
setupTooltips()
|
||||
})
|
||||
|
||||
watch(() => props.schema, (val) => {
|
||||
watch(
|
||||
() => props.schema,
|
||||
(val) => {
|
||||
schemaStore.setSchema(val)
|
||||
}, { deep: true })
|
||||
},
|
||||
{ deep: true },
|
||||
)
|
||||
|
||||
watch(() => props.data, (val) => {
|
||||
watch(
|
||||
() => props.data,
|
||||
(val) => {
|
||||
templateStore.setOverrideData(val ?? null)
|
||||
}, { deep: true })
|
||||
},
|
||||
{ deep: true },
|
||||
)
|
||||
|
||||
// Template: prop → store (only on reference change from parent)
|
||||
watch(() => props.modelValue, (val) => {
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
(val) => {
|
||||
if (syncing) return
|
||||
syncing = true
|
||||
templateStore.template = JSON.parse(JSON.stringify(val))
|
||||
nextTick(() => { syncing = false })
|
||||
})
|
||||
nextTick(() => {
|
||||
syncing = false
|
||||
})
|
||||
},
|
||||
)
|
||||
|
||||
// Template: store → emit
|
||||
watch(() => templateStore.template, (val) => {
|
||||
watch(
|
||||
() => templateStore.template,
|
||||
(val) => {
|
||||
if (syncing) return
|
||||
syncing = true
|
||||
emit('update:modelValue', JSON.parse(JSON.stringify(val)))
|
||||
nextTick(() => { syncing = false })
|
||||
}, { deep: true })
|
||||
nextTick(() => {
|
||||
syncing = false
|
||||
})
|
||||
},
|
||||
{ deep: true },
|
||||
)
|
||||
|
||||
// --- Error forwarding ---
|
||||
|
||||
@@ -85,7 +109,8 @@ function onCompileError(error: string | null) {
|
||||
function onKeyDown(e: KeyboardEvent) {
|
||||
const target = e.target as HTMLElement
|
||||
const tag = target?.tagName
|
||||
const isInput = tag === 'INPUT' || tag === 'TEXTAREA' || tag === 'SELECT' || target?.isContentEditable
|
||||
const isInput =
|
||||
tag === 'INPUT' || tag === 'TEXTAREA' || tag === 'SELECT' || target?.isContentEditable
|
||||
|
||||
// Delete / Backspace — çoklu seçim desteği
|
||||
if ((e.key === 'Delete' || e.key === 'Backspace') && editorStore.selectedElementIds.size > 0) {
|
||||
@@ -116,7 +141,11 @@ function onKeyDown(e: KeyboardEvent) {
|
||||
}
|
||||
|
||||
// Z-Order kısayolları
|
||||
if ((e.ctrlKey || e.metaKey) && editorStore.selectedElementId && editorStore.selectedElementId !== 'root') {
|
||||
if (
|
||||
(e.ctrlKey || e.metaKey) &&
|
||||
editorStore.selectedElementId &&
|
||||
editorStore.selectedElementId !== 'root'
|
||||
) {
|
||||
if (e.key === ']' && e.shiftKey) {
|
||||
e.preventDefault()
|
||||
templateStore.bringToFront(editorStore.selectedElementId)
|
||||
@@ -202,8 +231,20 @@ defineExpose({
|
||||
<div class="dreport-editor">
|
||||
<aside class="dreport-editor__sidebar dreport-editor__sidebar--left">
|
||||
<div class="sidebar-tabs">
|
||||
<button class="sidebar-tab" :class="{ 'sidebar-tab--active': leftTab === 'tools' }" @click="leftTab = 'tools'">Araclar</button>
|
||||
<button class="sidebar-tab" :class="{ 'sidebar-tab--active': leftTab === 'schema' }" @click="leftTab = 'schema'">Schema</button>
|
||||
<button
|
||||
class="sidebar-tab"
|
||||
:class="{ 'sidebar-tab--active': leftTab === 'tools' }"
|
||||
@click="leftTab = 'tools'"
|
||||
>
|
||||
Araclar
|
||||
</button>
|
||||
<button
|
||||
class="sidebar-tab"
|
||||
:class="{ 'sidebar-tab--active': leftTab === 'schema' }"
|
||||
@click="leftTab = 'schema'"
|
||||
>
|
||||
Schema
|
||||
</button>
|
||||
</div>
|
||||
<ToolboxPanel v-if="leftTab === 'tools'" />
|
||||
<SchemaTreePanel v-else />
|
||||
|
||||
@@ -24,13 +24,17 @@ const template = ref<Template>({
|
||||
page: { width: 210, height: 297 },
|
||||
fonts: ['Noto Sans'],
|
||||
root: {
|
||||
id: 'root', type: 'container',
|
||||
id: 'root',
|
||||
type: 'container',
|
||||
position: { type: 'flow' },
|
||||
size: { width: { type: 'auto' }, height: { type: 'auto' } },
|
||||
direction: 'column', gap: 0,
|
||||
direction: 'column',
|
||||
gap: 0,
|
||||
padding: { top: 0, right: 0, bottom: 0, left: 0 },
|
||||
align: 'stretch', justify: 'start',
|
||||
style: {}, children: [],
|
||||
align: 'stretch',
|
||||
justify: 'start',
|
||||
style: {},
|
||||
children: [],
|
||||
},
|
||||
})
|
||||
const data = ref<Record<string, unknown>>({})
|
||||
@@ -81,11 +85,7 @@ onMounted(() => {
|
||||
:data-render-ready="ready || undefined"
|
||||
:data-render-error="errorMsg || undefined"
|
||||
>
|
||||
<LayoutRenderer
|
||||
v-if="layout"
|
||||
:layout="layout"
|
||||
:scale="SCALE"
|
||||
/>
|
||||
<LayoutRenderer v-if="layout" :layout="layout" :scale="SCALE" />
|
||||
<div v-else-if="errorMsg" class="error">{{ errorMsg }}</div>
|
||||
<div v-else class="loading">Computing layout...</div>
|
||||
</div>
|
||||
|
||||
@@ -12,7 +12,13 @@ import { describe, it, expect, beforeEach, vi } from 'vitest'
|
||||
import { setActivePinia, createPinia } from 'pinia'
|
||||
import { useTemplateStore } from '../template'
|
||||
import { useEditorStore } from '../editor'
|
||||
import type { Template, StaticTextElement, ContainerElement, ImageElement, TemplateElement } from '../../core/types'
|
||||
import type {
|
||||
Template,
|
||||
StaticTextElement,
|
||||
ContainerElement,
|
||||
ImageElement,
|
||||
TemplateElement,
|
||||
} from '../../core/types'
|
||||
import { sz } from '../../core/types'
|
||||
|
||||
function createTestTemplate(): Template {
|
||||
@@ -392,52 +398,52 @@ describe('3.2 Z-Order controls', () => {
|
||||
const store = setupThreeElements()
|
||||
// Sıra: [a, b, c] → bringForward(a) → [b, a, c]
|
||||
store.bringForward('a')
|
||||
expect(store.template.root.children.map(c => c.id)).toEqual(['b', 'a', 'c'])
|
||||
expect(store.template.root.children.map((c) => c.id)).toEqual(['b', 'a', 'c'])
|
||||
})
|
||||
|
||||
it('sendBackward moves element one step down', () => {
|
||||
const store = setupThreeElements()
|
||||
// Sıra: [a, b, c] → sendBackward(c) → [a, c, b]
|
||||
store.sendBackward('c')
|
||||
expect(store.template.root.children.map(c => c.id)).toEqual(['a', 'c', 'b'])
|
||||
expect(store.template.root.children.map((c) => c.id)).toEqual(['a', 'c', 'b'])
|
||||
})
|
||||
|
||||
it('bringToFront moves element to end', () => {
|
||||
const store = setupThreeElements()
|
||||
// Sıra: [a, b, c] → bringToFront(a) → [b, c, a]
|
||||
store.bringToFront('a')
|
||||
expect(store.template.root.children.map(c => c.id)).toEqual(['b', 'c', 'a'])
|
||||
expect(store.template.root.children.map((c) => c.id)).toEqual(['b', 'c', 'a'])
|
||||
})
|
||||
|
||||
it('sendToBack moves element to beginning', () => {
|
||||
const store = setupThreeElements()
|
||||
// Sıra: [a, b, c] → sendToBack(c) → [c, a, b]
|
||||
store.sendToBack('c')
|
||||
expect(store.template.root.children.map(c => c.id)).toEqual(['c', 'a', 'b'])
|
||||
expect(store.template.root.children.map((c) => c.id)).toEqual(['c', 'a', 'b'])
|
||||
})
|
||||
|
||||
it('bringForward on last element is no-op', () => {
|
||||
const store = setupThreeElements()
|
||||
store.bringForward('c')
|
||||
expect(store.template.root.children.map(c => c.id)).toEqual(['a', 'b', 'c'])
|
||||
expect(store.template.root.children.map((c) => c.id)).toEqual(['a', 'b', 'c'])
|
||||
})
|
||||
|
||||
it('sendBackward on first element is no-op', () => {
|
||||
const store = setupThreeElements()
|
||||
store.sendBackward('a')
|
||||
expect(store.template.root.children.map(c => c.id)).toEqual(['a', 'b', 'c'])
|
||||
expect(store.template.root.children.map((c) => c.id)).toEqual(['a', 'b', 'c'])
|
||||
})
|
||||
|
||||
it('bringToFront on last element is no-op', () => {
|
||||
const store = setupThreeElements()
|
||||
store.bringToFront('c')
|
||||
expect(store.template.root.children.map(c => c.id)).toEqual(['a', 'b', 'c'])
|
||||
expect(store.template.root.children.map((c) => c.id)).toEqual(['a', 'b', 'c'])
|
||||
})
|
||||
|
||||
it('sendToBack on first element is no-op', () => {
|
||||
const store = setupThreeElements()
|
||||
store.sendToBack('a')
|
||||
expect(store.template.root.children.map(c => c.id)).toEqual(['a', 'b', 'c'])
|
||||
expect(store.template.root.children.map((c) => c.id)).toEqual(['a', 'b', 'c'])
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -78,7 +78,7 @@ describe('useTemplateStore', () => {
|
||||
store.addChild('root', createTextElement('b', 'B'))
|
||||
store.addChild('root', createTextElement('c', 'C'), 1)
|
||||
|
||||
expect(store.template.root.children.map(c => c.id)).toEqual(['a', 'c', 'b'])
|
||||
expect(store.template.root.children.map((c) => c.id)).toEqual(['a', 'c', 'b'])
|
||||
})
|
||||
|
||||
it('removeElement removes element', () => {
|
||||
@@ -133,7 +133,7 @@ describe('useTemplateStore', () => {
|
||||
|
||||
store.reorderChild('root', 0, 2)
|
||||
|
||||
expect(store.template.root.children.map(c => c.id)).toEqual(['b', 'c', 'a'])
|
||||
expect(store.template.root.children.map((c) => c.id)).toEqual(['b', 'c', 'a'])
|
||||
})
|
||||
|
||||
it('exportTemplate returns valid JSON', () => {
|
||||
|
||||
@@ -1,6 +1,12 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref, computed } from 'vue'
|
||||
import type { Template, TemplateElement, ContainerElement, SizeConstraint, PositionMode } from '../core/types'
|
||||
import type {
|
||||
Template,
|
||||
TemplateElement,
|
||||
ContainerElement,
|
||||
SizeConstraint,
|
||||
PositionMode,
|
||||
} from '../core/types'
|
||||
import { findElementById, findParent, isContainer, sz } from '../core/types'
|
||||
import { generateMockData } from '../core/mock-data-generator'
|
||||
import { useUndoRedo } from '../composables/useUndoRedo'
|
||||
@@ -132,7 +138,7 @@ export const useTemplateStore = defineStore('template', () => {
|
||||
function removeElement(elementId: string) {
|
||||
const parent = getParent(elementId)
|
||||
if (!parent) return
|
||||
const idx = parent.children.findIndex(c => c.id === elementId)
|
||||
const idx = parent.children.findIndex((c) => c.id === elementId)
|
||||
if (idx !== -1) {
|
||||
parent.children.splice(idx, 1)
|
||||
bumpLayoutVersion()
|
||||
@@ -146,7 +152,7 @@ export const useTemplateStore = defineStore('template', () => {
|
||||
// Ağaçtan kaldır (bump'sız)
|
||||
const parent = getParent(elementId)
|
||||
if (parent) {
|
||||
const idx = parent.children.findIndex(c => c.id === elementId)
|
||||
const idx = parent.children.findIndex((c) => c.id === elementId)
|
||||
if (idx !== -1) parent.children.splice(idx, 1)
|
||||
}
|
||||
// Hedef container'a ekle (bump'sız)
|
||||
@@ -202,7 +208,7 @@ export const useTemplateStore = defineStore('template', () => {
|
||||
function bringForward(elementId: string) {
|
||||
const parent = getParent(elementId)
|
||||
if (!parent) return
|
||||
const idx = parent.children.findIndex(c => c.id === elementId)
|
||||
const idx = parent.children.findIndex((c) => c.id === elementId)
|
||||
if (idx < 0 || idx >= parent.children.length - 1) return
|
||||
reorderChild(parent.id, idx, idx + 1)
|
||||
}
|
||||
@@ -211,7 +217,7 @@ export const useTemplateStore = defineStore('template', () => {
|
||||
function sendBackward(elementId: string) {
|
||||
const parent = getParent(elementId)
|
||||
if (!parent) return
|
||||
const idx = parent.children.findIndex(c => c.id === elementId)
|
||||
const idx = parent.children.findIndex((c) => c.id === elementId)
|
||||
if (idx <= 0) return
|
||||
reorderChild(parent.id, idx, idx - 1)
|
||||
}
|
||||
@@ -220,7 +226,7 @@ export const useTemplateStore = defineStore('template', () => {
|
||||
function bringToFront(elementId: string) {
|
||||
const parent = getParent(elementId)
|
||||
if (!parent) return
|
||||
const idx = parent.children.findIndex(c => c.id === elementId)
|
||||
const idx = parent.children.findIndex((c) => c.id === elementId)
|
||||
if (idx < 0 || idx >= parent.children.length - 1) return
|
||||
reorderChild(parent.id, idx, parent.children.length - 1)
|
||||
}
|
||||
@@ -229,7 +235,7 @@ export const useTemplateStore = defineStore('template', () => {
|
||||
function sendToBack(elementId: string) {
|
||||
const parent = getParent(elementId)
|
||||
if (!parent) return
|
||||
const idx = parent.children.findIndex(c => c.id === elementId)
|
||||
const idx = parent.children.findIndex((c) => c.id === elementId)
|
||||
if (idx <= 0) return
|
||||
reorderChild(parent.id, idx, 0)
|
||||
}
|
||||
@@ -251,7 +257,11 @@ export const useTemplateStore = defineStore('template', () => {
|
||||
if (!parsed.root || parsed.root.type !== 'container') {
|
||||
throw new Error('Geçersiz şablon: root alanı eksik veya container değil')
|
||||
}
|
||||
if (!parsed.page || typeof parsed.page.width !== 'number' || typeof parsed.page.height !== 'number') {
|
||||
if (
|
||||
!parsed.page ||
|
||||
typeof parsed.page.width !== 'number' ||
|
||||
typeof parsed.page.height !== 'number'
|
||||
) {
|
||||
throw new Error('Geçersiz şablon: page alanı eksik veya geçersiz')
|
||||
}
|
||||
template.value = parsed
|
||||
|
||||
@@ -44,7 +44,8 @@
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html, body {
|
||||
html,
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
|
||||
@@ -2,7 +2,12 @@
|
||||
/// Template JSON + Data JSON → Layout WASM → LayoutResult
|
||||
/// Font loading is dynamic — fetches from backend API based on template needs.
|
||||
|
||||
import init, { loadFonts, addFonts, computeLayout, generateBarcode } from '../core/wasm-layout/dreport_layout.js'
|
||||
import init, {
|
||||
loadFonts,
|
||||
addFonts,
|
||||
computeLayout,
|
||||
generateBarcode,
|
||||
} from '../core/wasm-layout/dreport_layout.js'
|
||||
import type { LayoutResult } from '../core/layout-types'
|
||||
|
||||
let initPromise: Promise<void> | null = null
|
||||
@@ -35,7 +40,9 @@ async function doInit() {
|
||||
fontCatalog = await res.json()
|
||||
console.log(`[layout-worker] Font kataloğu yüklendi (${fontCatalog.length} aile)`)
|
||||
} else {
|
||||
console.warn(`[layout-worker] Font kataloğu alınamadı (HTTP ${res.status}), static fallback deneniyor`)
|
||||
console.warn(
|
||||
`[layout-worker] Font kataloğu alınamadı (HTTP ${res.status}), static fallback deneniyor`,
|
||||
)
|
||||
await loadStaticFallback()
|
||||
return
|
||||
}
|
||||
@@ -68,7 +75,7 @@ async function loadStaticFallback() {
|
||||
if (res.ok) {
|
||||
buffers.push(new Uint8Array(await res.arrayBuffer()))
|
||||
}
|
||||
})
|
||||
}),
|
||||
)
|
||||
|
||||
if (buffers.length > 0) {
|
||||
@@ -81,13 +88,13 @@ async function loadStaticFallback() {
|
||||
|
||||
/** Load all variants of given families from the API into WASM */
|
||||
async function ensureFamiliesLoaded(families: string[]): Promise<void> {
|
||||
const toLoad = families.filter(f => !loadedFamilies.has(f.toLowerCase()))
|
||||
const toLoad = families.filter((f) => !loadedFamilies.has(f.toLowerCase()))
|
||||
if (toLoad.length === 0) return
|
||||
|
||||
const buffers: Uint8Array[] = []
|
||||
|
||||
for (const family of toLoad) {
|
||||
const info = fontCatalog.find(f => f.family.toLowerCase() === family.toLowerCase())
|
||||
const info = fontCatalog.find((f) => f.family.toLowerCase() === family.toLowerCase())
|
||||
if (!info) {
|
||||
console.warn(`[layout-worker] Font ailesi bulunamadı: ${family}`)
|
||||
continue
|
||||
@@ -132,7 +139,15 @@ function ensureInit(): Promise<void> {
|
||||
|
||||
type WorkerMessage =
|
||||
| { type: 'compile'; templateJson: string; dataJson: string; id: number }
|
||||
| { type: 'barcode'; format: string; value: string; width: number; height: number; includeText: boolean; id: number }
|
||||
| {
|
||||
type: 'barcode'
|
||||
format: string
|
||||
value: string
|
||||
width: number
|
||||
height: number
|
||||
includeText: boolean
|
||||
id: number
|
||||
}
|
||||
| { type: 'configure'; fontApiBase?: string }
|
||||
|
||||
self.onmessage = async (e: MessageEvent<WorkerMessage>) => {
|
||||
|
||||
Reference in New Issue
Block a user