459 lines
32 KiB
PHP
459 lines
32 KiB
PHP
<?= $this->extend('layouts/main') ?>
|
|
|
|
<?php
|
|
$cRow = is_array($chart ?? null) ? $chart : [];
|
|
$bootstrap = [
|
|
'mode' => $mode,
|
|
'chartId' => isset($cRow['id']) ? (int) $cRow['id'] : null,
|
|
'savedQueryId' => (int) ($cRow['saved_query_id'] ?? $preselectQueryId ?? 0),
|
|
'chartType' => (string) ($cRow['chart_type'] ?? 'line'),
|
|
'name' => (string) ($cRow['name'] ?? ''),
|
|
'description' => (string) ($cRow['description'] ?? ''),
|
|
'xField' => (string) ($cRow['x_field'] ?? ''),
|
|
'yField' => (string) ($cRow['y_field'] ?? ''),
|
|
'groupField' => (string) ($cRow['group_field'] ?? ''),
|
|
'valueField' => (string) ($cRow['value_field'] ?? ''),
|
|
'refreshInterval' => (int) ($cRow['refresh_interval'] ?? 0),
|
|
'display' => $displayConfig ?? [],
|
|
];
|
|
$bootstrapJson = json_encode($bootstrap, JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_AMP | JSON_UNESCAPED_UNICODE) ?: '{}';
|
|
?>
|
|
|
|
<?= $this->section('styles') ?>
|
|
<link rel="stylesheet" href="<?= base_url('assets/css/chart-builder.css') ?>">
|
|
<?= $this->endSection() ?>
|
|
|
|
<?= $this->section('content') ?>
|
|
<header class="cb-page-header cb-page-header--center">
|
|
<div>
|
|
<h1 class="cb-page-title"><?= $mode === 'edit' ? 'Edit chart' : 'Create chart' ?></h1>
|
|
<p class="cb-page-subtitle mb-0">Visualization-first composer: connect query, pick recommended chart, and tune with live preview.</p>
|
|
</div>
|
|
<div class="d-flex flex-wrap gap-2 justify-content-end">
|
|
<?php if ($mode === 'edit' && ! empty($cRow['id'])): ?>
|
|
<button type="button" class="btn btn-outline-secondary btn-sm" data-bs-toggle="modal" data-bs-target="#modalShare" data-share-type="chart" data-resource-id="<?= (int) $cRow['id'] ?>">Share</button>
|
|
<?php endif; ?>
|
|
<a href="<?= base_url('chart') ?>" class="btn cb-settings-back">All charts</a>
|
|
</div>
|
|
</header>
|
|
|
|
<div class="cb-page-body">
|
|
<script type="application/json" id="cb-bootstrap-json"><?= $bootstrapJson ?></script>
|
|
<div class="cb-chart-builder" x-data="chartBuilder(JSON.parse(document.getElementById('cb-bootstrap-json').textContent.trim()))" x-init="init()">
|
|
<?php if (session()->getFlashdata('success')): ?>
|
|
<div class="alert alert-success"><?= esc(session()->getFlashdata('success')) ?></div>
|
|
<?php endif; ?>
|
|
<?php if (session()->getFlashdata('error')): ?>
|
|
<div class="alert alert-danger"><?= esc(session()->getFlashdata('error')) ?></div>
|
|
<?php endif; ?>
|
|
|
|
<form id="chart-builder-form" method="post"
|
|
:action="boot.mode === 'edit' ? '<?= base_url('chart/update/') ?>' + boot.chartId : '<?= base_url('chart/store') ?>'"
|
|
@submit="beforeSubmit($event)">
|
|
<?= csrf_field() ?>
|
|
|
|
<input type="hidden" name="display_config" :value="displayJson()">
|
|
<input type="hidden" name="chart_type" :value="chartType">
|
|
<input type="hidden" name="saved_query_id" :value="savedQueryId">
|
|
<input type="hidden" name="x_field" :value="xField">
|
|
<input type="hidden" name="y_field" :value="yField">
|
|
<input type="hidden" name="group_field" :value="groupField">
|
|
<input type="hidden" name="value_field" :value="valueField">
|
|
|
|
<div class="cb-cb-card mb-3">
|
|
<div class="cb-cb-card-b cb-cb-query-compact">
|
|
<div class="cb-cb-query-row">
|
|
<div class="cb-cb-query-main">
|
|
<label class="form-label small fw-semibold mb-1">Saved query</label>
|
|
<select class="form-select" x-model.number="savedQueryId" @change="onQueryChange()" required>
|
|
<option value="">— Select a query —</option>
|
|
<?php foreach ($queries as $q): ?>
|
|
<option value="<?= (int) $q['id'] ?>"><?= esc($q['name']) ?> (<?= esc($q['data_source_name'] ?? '') ?>)</option>
|
|
<?php endforeach; ?>
|
|
</select>
|
|
</div>
|
|
<div class="cb-cb-query-actions">
|
|
<div class="form-check form-switch mb-0">
|
|
<input class="form-check-input" type="checkbox" id="cbAutoRun" x-model="autoRunPreview">
|
|
<label class="form-check-label small" for="cbAutoRun">Auto-run preview</label>
|
|
</div>
|
|
<button type="button" class="btn btn-dark btn-sm" @click.prevent="runPreview()" :disabled="!savedQueryId || previewLoading">
|
|
<span x-show="!previewLoading">Run</span>
|
|
<span x-show="previewLoading" class="spinner-border spinner-border-sm"></span>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<div class="cb-cb-query-vars mt-2" x-show="variableDefs.length" x-cloak>
|
|
<div class="p-2 rounded-3 bg-light border">
|
|
<div class="small fw-bold mb-2">Query variables</div>
|
|
<div class="cb-cb-var-grid">
|
|
<template x-for="v in variableDefs" :key="v.name">
|
|
<div class="cb-cb-var-pill" :class="{ 'is-filled': String(variables[v.name] || '').trim() !== '' }">
|
|
<span class="cb-cb-var-key" x-text="v.label || v.name"></span>
|
|
<input
|
|
type="text"
|
|
class="cb-cb-var-input"
|
|
:placeholder="v.default_value || ''"
|
|
x-model="variables[v.name]"
|
|
@input="queueRenderPreview()"
|
|
>
|
|
<button
|
|
type="button"
|
|
class="cb-cb-var-clear"
|
|
x-show="String(variables[v.name] || '').trim() !== ''"
|
|
x-cloak
|
|
@click="variables[v.name] = ''; queueRenderPreview()"
|
|
title="Clear"
|
|
aria-label="Clear variable value"
|
|
>
|
|
<span class="material-symbols-outlined">close</span>
|
|
</button>
|
|
</div>
|
|
</template>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<p class="text-danger small mt-2 mb-0" x-show="previewError" x-text="previewError"></p>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="row g-3 align-items-start cb-cb-main-row">
|
|
<div class="col-lg-4">
|
|
<div class="cb-cb-card cb-cb-config-card">
|
|
<div class="cb-cb-config-tabs">
|
|
<button type="button" class="cb-cb-tab-btn" :class="{ active: configTab === 'data' }" @click="configTab = 'data'">Data</button>
|
|
<button type="button" class="cb-cb-tab-btn" :class="{ active: configTab === 'display' }" @click="configTab = 'display'">Display</button>
|
|
</div>
|
|
<div class="cb-cb-card-b cb-cb-config-body">
|
|
<div x-show="configTab === 'data'" x-cloak>
|
|
<div class="cb-cb-section mb-3">
|
|
<div class="small fw-bold mb-2">Recommended charts</div>
|
|
<div class="d-flex flex-wrap gap-2" x-show="recommendedTypes.length" x-cloak>
|
|
<template x-for="rid in recommendedTypes" :key="rid">
|
|
<button type="button" class="cb-cb-type cb-cb-type--compact" :class="{ selected: chartType === rid }" @click="selectChartType(rid)">
|
|
<span x-text="labelForType(rid)"></span>
|
|
</button>
|
|
</template>
|
|
</div>
|
|
<p class="small text-muted mb-0" x-show="!recommendedTypes.length">Run query once to get chart recommendations based on your columns.</p>
|
|
<details class="mt-2">
|
|
<summary class="small fw-semibold">All chart types</summary>
|
|
<div class="cb-cb-type-grid mt-2">
|
|
<template x-for="t in chartTypes" :key="t.id">
|
|
<button type="button" class="cb-cb-type" :class="{ selected: chartType === t.id }" @click="selectChartType(t.id)">
|
|
<span class="cb-cb-type-ico" x-text="t.icon"></span>
|
|
<span x-text="t.label"></span>
|
|
</button>
|
|
</template>
|
|
</div>
|
|
</details>
|
|
<div class="cb-cb-hint mt-2"><span x-text="typeHint()"></span></div>
|
|
</div>
|
|
|
|
<div class="cb-cb-section">
|
|
<div class="small fw-bold mb-2">Field mapping</div>
|
|
<template x-if="!columns.length">
|
|
<p class="text-muted small mb-0">Run query first to load columns and enable mapping.</p>
|
|
</template>
|
|
<template x-if="columns.length">
|
|
<div>
|
|
<div class="cb-cb-fm-row" x-show="needsXY()">
|
|
<div><span class="fw-semibold small">X axis</span><div class="text-muted small">Category / date</div></div>
|
|
<select class="form-select form-select-sm" x-model="xField" @change="queueRenderPreview()">
|
|
<option value="">—</option>
|
|
<template x-for="c in columns" :key="c"><option :value="c" x-text="c"></option></template>
|
|
</select>
|
|
</div>
|
|
<div class="cb-cb-fm-row" x-show="needsXY()">
|
|
<div><span class="fw-semibold small">Y axis</span><div class="text-muted small">Numeric values</div></div>
|
|
<select class="form-select form-select-sm" x-model="yField" @change="queueRenderPreview()">
|
|
<option value="">—</option>
|
|
<template x-for="c in columns" :key="c"><option :value="c" x-text="c"></option></template>
|
|
</select>
|
|
</div>
|
|
<div class="cb-cb-fm-row" x-show="showGroup()">
|
|
<div><span class="fw-semibold small">Group / series</span><div class="text-muted small">Optional split</div></div>
|
|
<select class="form-select form-select-sm" x-model="groupField" @change="queueRenderPreview()">
|
|
<option value="">— None —</option>
|
|
<template x-for="c in columns" :key="c"><option :value="c" x-text="c"></option></template>
|
|
</select>
|
|
</div>
|
|
<div class="cb-cb-fm-row" x-show="chartType === 'bubble'">
|
|
<div><span class="fw-semibold small">Bubble size (Z)</span></div>
|
|
<select class="form-select form-select-sm" x-model="valueField" @change="queueRenderPreview()">
|
|
<option value="">—</option>
|
|
<template x-for="c in columns" :key="c"><option :value="c" x-text="c"></option></template>
|
|
</select>
|
|
</div>
|
|
<div class="cb-cb-fm-row" x-show="chartType === 'gauge' || chartType === 'kpi_card'">
|
|
<div><span class="fw-semibold small">Value column</span></div>
|
|
<select class="form-select form-select-sm" x-model="valueField" @change="queueRenderPreview()">
|
|
<option value="">—</option>
|
|
<template x-for="c in columns" :key="c"><option :value="c" x-text="c"></option></template>
|
|
</select>
|
|
</div>
|
|
<div class="cb-cb-fm-row" x-show="chartType === 'combo'">
|
|
<div><span class="fw-semibold small">Line series (2nd metric)</span></div>
|
|
<select class="form-select form-select-sm" x-model="display.secondary_y_field" @change="queueRenderPreview()">
|
|
<option value="">—</option>
|
|
<template x-for="c in columns" :key="c"><option :value="c" x-text="c"></option></template>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
</div>
|
|
</div>
|
|
|
|
<div x-show="configTab === 'display'" x-cloak>
|
|
<div class="cb-cb-section mb-3">
|
|
<div class="small fw-bold mb-2">Basic style</div>
|
|
<div class="mb-2">
|
|
<label class="form-label small">Chart title</label>
|
|
<input type="text" class="form-control form-control-sm" x-model="display.title" @input="queueRenderPreview()" placeholder="Optional">
|
|
</div>
|
|
<div class="mb-2">
|
|
<label class="form-label small">Number format</label>
|
|
<select class="form-select form-select-sm" x-model="display.number_format" @change="queueRenderPreview()">
|
|
<option value="auto">Auto</option>
|
|
<option value="inr">₹ INR</option>
|
|
<option value="usd">$ USD</option>
|
|
<option value="percent">%</option>
|
|
<option value="compact">K / M / B</option>
|
|
</select>
|
|
</div>
|
|
<div class="form-check form-switch mb-1">
|
|
<input class="form-check-input" type="checkbox" id="lg" x-model="display.show_legend" @change="queueRenderPreview()">
|
|
<label class="form-check-label small" for="lg">Legend</label>
|
|
</div>
|
|
<div class="form-check form-switch mb-1">
|
|
<input class="form-check-input" type="checkbox" id="dl" x-model="display.show_data_labels" @change="queueRenderPreview()">
|
|
<label class="form-check-label small" for="dl">Data labels</label>
|
|
</div>
|
|
<div class="form-check form-switch mb-0">
|
|
<input class="form-check-input" type="checkbox" id="gr" x-model="display.show_grid" @change="queueRenderPreview()">
|
|
<label class="form-check-label small" for="gr">Grid lines</label>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="cb-cb-section">
|
|
<div class="small fw-bold mb-2">Advanced options</div>
|
|
<button type="button" class="cb-cb-advanced-toggle" data-bs-toggle="collapse" data-bs-target="#cbAdvancedStyleInline" aria-expanded="false" aria-controls="cbAdvancedStyleInline">
|
|
<span class="material-symbols-outlined">tune</span>
|
|
<span>Advanced style controls</span>
|
|
</button>
|
|
<div id="cbAdvancedStyleInline" class="collapse mt-2">
|
|
<div class="row g-3">
|
|
<div class="col-12">
|
|
<label class="form-label small">Subtitle</label>
|
|
<input type="text" class="form-control form-control-sm" x-model="display.subtitle" @input="queueRenderPreview()">
|
|
</div>
|
|
<div class="col-6">
|
|
<label class="form-label small">Decimal places</label>
|
|
<select class="form-select form-select-sm" x-model.number="display.decimal_places" @change="queueRenderPreview()">
|
|
<option value="0">0</option>
|
|
<option value="1">1</option>
|
|
<option value="2">2</option>
|
|
</select>
|
|
</div>
|
|
<div class="col-6">
|
|
<label class="form-label small">Colors</label>
|
|
<select class="form-select form-select-sm" x-model="display.color_mode" @change="ensurePaletteColors(); queueRenderPreview();">
|
|
<option value="preset">Theme</option>
|
|
<option value="custom">Custom</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
<div class="row g-3 mt-1" x-show="display.color_mode === 'preset'">
|
|
<div class="col-12">
|
|
<label class="form-label small">Palette</label>
|
|
<select class="form-select form-select-sm" x-model="display.palette" @change="queueRenderPreview()">
|
|
<option value="mono">Mono</option>
|
|
<option value="ocean">Ocean</option>
|
|
<option value="forest">Forest</option>
|
|
<option value="sunset">Sunset</option>
|
|
<option value="slate">Slate</option>
|
|
<option value="violet">Violet</option>
|
|
<option value="rose">Rose</option>
|
|
<option value="teal">Teal</option>
|
|
<option value="amber">Amber</option>
|
|
<option value="indigo">Indigo</option>
|
|
<option value="cool_gray">Cool Gray</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
<div class="mt-2" x-show="display.color_mode === 'custom'">
|
|
<label class="form-label small">Swatches</label>
|
|
<div class="cb-cb-swatch-grid">
|
|
<template x-for="idx in [0,1,2,3,4,5,6,7,8,9,10,11]" :key="idx">
|
|
<input type="color" class="form-control form-control-color cb-cb-color-swatch" :title="'Color ' + (idx + 1)" x-model="display.palette_colors[idx]" @input="queueRenderPreview()">
|
|
</template>
|
|
</div>
|
|
</div>
|
|
<div class="row g-3 mt-1">
|
|
<div class="col-6" x-show="needsXY()">
|
|
<label class="form-label small">Y min</label>
|
|
<input type="text" class="form-control form-control-sm" x-model="display.y_min" @input="queueRenderPreview()" placeholder="Auto">
|
|
</div>
|
|
<div class="col-6" x-show="needsXY()">
|
|
<label class="form-label small">Y max</label>
|
|
<input type="text" class="form-control form-control-sm" x-model="display.y_max" @input="queueRenderPreview()" placeholder="Auto">
|
|
</div>
|
|
</div>
|
|
<div class="small fw-semibold text-muted mt-3 mb-2">Chart behavior</div>
|
|
<div class="d-flex flex-wrap gap-3">
|
|
<div class="form-check form-switch mb-0">
|
|
<input class="form-check-input" type="checkbox" id="advLegend" x-model="display.show_legend" @change="queueRenderPreview()">
|
|
<label class="form-check-label small" for="advLegend">Legend</label>
|
|
</div>
|
|
<div class="form-check form-switch mb-0">
|
|
<input class="form-check-input" type="checkbox" id="advLabels" x-model="display.show_data_labels" @change="queueRenderPreview()">
|
|
<label class="form-check-label small" for="advLabels">Data labels</label>
|
|
</div>
|
|
<div class="form-check form-switch mb-0">
|
|
<input class="form-check-input" type="checkbox" id="advGrid" x-model="display.show_grid" @change="queueRenderPreview()">
|
|
<label class="form-check-label small" for="advGrid">Grid lines</label>
|
|
</div>
|
|
<div class="form-check form-switch mb-0" x-show="chartType === 'line' || chartType === 'area'">
|
|
<input class="form-check-input" type="checkbox" id="advSmooth" x-model="display.smooth" @change="queueRenderPreview()">
|
|
<label class="form-check-label small" for="advSmooth">Smooth curves</label>
|
|
</div>
|
|
<div class="form-check form-switch mb-0" x-show="chartType === 'line' || chartType === 'area'">
|
|
<input class="form-check-input" type="checkbox" id="advStepline" x-model="display.stepline" @change="queueRenderPreview()">
|
|
<label class="form-check-label small" for="advStepline">Step line</label>
|
|
</div>
|
|
<div class="form-check form-switch mb-0" x-show="chartType === 'bar'">
|
|
<input class="form-check-input" type="checkbox" id="advHorizontalBar" x-model="display.horizontal_bar" @change="queueRenderPreview()">
|
|
<label class="form-check-label small" for="advHorizontalBar">Horizontal bars</label>
|
|
</div>
|
|
<div class="form-check form-switch mb-0" x-show="chartType === 'bar' || chartType === 'area' || chartType === 'line' || chartType === 'spline' || chartType === 'stepline'">
|
|
<input class="form-check-input" type="checkbox" id="advStacked" x-model="display.stacked" @change="queueRenderPreview()">
|
|
<label class="form-check-label small" for="advStacked">Stacked</label>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="col-lg-8">
|
|
<div class="cb-cb-card cb-cb-preview-card">
|
|
<div class="cb-cb-card-h">
|
|
<span class="fw-bold">Live preview</span>
|
|
<div class="d-flex gap-2 align-items-center">
|
|
<span class="small text-muted" x-text="renderMeta"></span>
|
|
<button type="button" class="btn btn-outline-secondary btn-sm d-inline-flex align-items-center justify-content-center" style="width:36px;height:32px;" id="cbPreviewFsBtn" title="Fullscreen preview" aria-label="Fullscreen preview">
|
|
<span class="material-symbols-outlined" id="cbPreviewFsIcon" style="font-size:18px;">open_in_full</span>
|
|
</button>
|
|
<button type="button" class="btn btn-outline-secondary btn-sm d-inline-flex align-items-center justify-content-center" style="width:36px;height:32px;" title="Refresh preview" aria-label="Refresh preview" @click.prevent="renderLivePreview()" :disabled="renderLoading || !columns.length">
|
|
<span class="material-symbols-outlined" style="font-size:18px;">refresh</span>
|
|
</button>
|
|
<?php if ($mode === 'edit' && ! empty($cRow['id'])): ?>
|
|
<button type="button" class="btn btn-sm btn-outline-secondary d-inline-flex align-items-center justify-content-center" style="width:36px;height:32px;" id="cbExportPng" title="Export PNG" aria-label="Export PNG">
|
|
<span class="material-symbols-outlined" style="font-size:18px;">image</span>
|
|
</button>
|
|
<a class="btn btn-sm btn-outline-secondary d-inline-flex align-items-center justify-content-center" style="width:36px;height:32px;" href="<?= base_url('chart/' . (int) $cRow['id'] . '/export?format=csv') ?>" title="Export CSV" aria-label="Export CSV">
|
|
<span class="material-symbols-outlined" style="font-size:18px;">description</span>
|
|
</a>
|
|
<a class="btn btn-sm btn-outline-secondary d-inline-flex align-items-center justify-content-center" style="width:36px;height:32px;" href="<?= base_url('chart/' . (int) $cRow['id'] . '/export?format=excel') ?>" title="Export Excel" aria-label="Export Excel">
|
|
<span class="material-symbols-outlined" style="font-size:18px;">table_view</span>
|
|
</a>
|
|
<?php endif; ?>
|
|
</div>
|
|
</div>
|
|
<div class="cb-cb-card-b">
|
|
<div id="cb-apex-preview" class="cb-cb-preview-host p-2"></div>
|
|
<p class="text-danger small mt-2 mb-0" x-show="renderError" x-text="renderError"></p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="cb-cb-card mt-3">
|
|
<div class="cb-cb-card-h"><span class="fw-bold">Save chart</span></div>
|
|
<div class="cb-cb-card-b">
|
|
<div class="row g-3 mt-1">
|
|
<div class="col-lg-8">
|
|
<label class="form-label small">Chart name</label>
|
|
<input type="text" class="form-control" name="name" x-model="chartName" required minlength="2" maxlength="200">
|
|
</div>
|
|
<div class="col-lg-4">
|
|
<label class="form-label small">Auto-refresh (dashboards)</label>
|
|
<select class="form-select form-select-sm" name="refresh_interval" x-model.number="refreshInterval">
|
|
<option :value="0">Manual</option>
|
|
<option :value="60">Every minute</option>
|
|
<option :value="300">5 minutes</option>
|
|
<option :value="900">15 minutes</option>
|
|
<option :value="3600">1 hour</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="row g-3 mt-1 align-items-end">
|
|
<div class="col-lg-9">
|
|
<label class="form-label small">Description</label>
|
|
<textarea class="form-control form-control-sm" name="description" rows="2" x-model="chartDescription"></textarea>
|
|
</div>
|
|
<div class="col-lg-3 d-grid">
|
|
<button type="submit" class="btn btn-dark"><?= $mode === 'edit' ? 'Update chart' : 'Save chart' ?></button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
<?= $this->endSection() ?>
|
|
|
|
<?= $this->section('scripts') ?>
|
|
<script src="https://cdn.jsdelivr.net/npm/file-saver@2.0.5/dist/FileSaver.min.js"></script>
|
|
<?= $this->include('chart/builder_scripts') ?>
|
|
<script>
|
|
document.getElementById('cbExportPng')?.addEventListener('click', async function () {
|
|
const el = document.getElementById('cb-apex-preview');
|
|
const c = el && el._apexChart;
|
|
if (!c || typeof c.dataURI !== 'function') {
|
|
alert('Run a live Apex preview first.');
|
|
return;
|
|
}
|
|
try {
|
|
const { imgURI } = await c.dataURI({ scale: 2, width: el.offsetWidth || undefined });
|
|
const res = await fetch(imgURI);
|
|
const blob = await res.blob();
|
|
if (typeof saveAs === 'function') saveAs(blob, 'chart.png');
|
|
else window.open(imgURI, '_blank');
|
|
} catch (e) {
|
|
alert('Could not export PNG.');
|
|
}
|
|
});
|
|
|
|
(function () {
|
|
const btn = document.getElementById('cbPreviewFsBtn');
|
|
const icon = document.getElementById('cbPreviewFsIcon');
|
|
const card = document.querySelector('.cb-cb-preview-card');
|
|
if (!btn || !icon || !card) return;
|
|
|
|
const sync = () => {
|
|
const open = card.classList.contains('cb-cb-preview-fullscreen');
|
|
icon.textContent = open ? 'close_fullscreen' : 'open_in_full';
|
|
btn.title = open ? 'Exit fullscreen' : 'Fullscreen preview';
|
|
btn.setAttribute('aria-label', btn.title);
|
|
document.body.classList.toggle('cb-cb-preview-fs-open', open);
|
|
};
|
|
|
|
btn.addEventListener('click', () => {
|
|
card.classList.toggle('cb-cb-preview-fullscreen');
|
|
sync();
|
|
});
|
|
|
|
document.addEventListener('keydown', (ev) => {
|
|
if (ev.key === 'Escape' && card.classList.contains('cb-cb-preview-fullscreen')) {
|
|
card.classList.remove('cb-cb-preview-fullscreen');
|
|
sync();
|
|
}
|
|
});
|
|
})();
|
|
</script>
|
|
<?= $this->endSection() ?>
|