forked from UKSOURCE/cms.lams
Add icon colection
This commit is contained in:
+98572
File diff suppressed because one or more lines are too long
@@ -0,0 +1,129 @@
|
|||||||
|
// public/js/icon-picker.js
|
||||||
|
(function () {
|
||||||
|
let ALL_ICONS = [];
|
||||||
|
let _targetInput = null;
|
||||||
|
|
||||||
|
// ── Init: inject modal + fetch icons ──────────────────────────────────────
|
||||||
|
function init() {
|
||||||
|
_injectModal();
|
||||||
|
_fetchIcons();
|
||||||
|
_bindSearch();
|
||||||
|
}
|
||||||
|
|
||||||
|
function _injectModal() {
|
||||||
|
if (document.getElementById('iconPickerModal')) return; // tránh inject 2 lần
|
||||||
|
|
||||||
|
document.body.insertAdjacentHTML('beforeend', `
|
||||||
|
<div class="modal fade" id="iconPickerModal" tabindex="-1">
|
||||||
|
<div class="modal-dialog modal-lg">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title"><i class="fas fa-icons me-2"></i>Choose an Icon</h5>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<input type="text" class="form-control mb-3" id="iconSearchInput" placeholder="Search icons... (e.g. phone, mail)">
|
||||||
|
<div id="iconPickerStatus" class="text-muted small mb-2"></div>
|
||||||
|
<div id="iconGrid" class="d-flex flex-wrap gap-2"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function _fetchIcons() {
|
||||||
|
try {
|
||||||
|
const res = await fetch('/js/fa-icons.json');
|
||||||
|
if (!res.ok) throw new Error('HTTP ' + res.status);
|
||||||
|
const json = await res.json();
|
||||||
|
|
||||||
|
ALL_ICONS = Object.entries(json).flatMap(([name, meta]) =>
|
||||||
|
(meta.styles || [])
|
||||||
|
.filter(s => ['solid', 'regular', 'brands'].includes(s))
|
||||||
|
.map(style => ({
|
||||||
|
value: `fa-${style} fa-${name}`,
|
||||||
|
label: meta.label || name,
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log('[IconPicker] loaded:', ALL_ICONS.length, 'icons');
|
||||||
|
} catch (err) {
|
||||||
|
console.error('[IconPicker] fetch failed:', err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function _bindSearch() {
|
||||||
|
document.addEventListener('input', function (e) {
|
||||||
|
if (e.target.id !== 'iconSearchInput') return;
|
||||||
|
const q = e.target.value.toLowerCase().trim();
|
||||||
|
const filtered = q
|
||||||
|
? ALL_ICONS.filter(i => i.label.toLowerCase().includes(q) || i.value.includes(q))
|
||||||
|
: ALL_ICONS;
|
||||||
|
console.log('[IconPicker] search:', q, '→', filtered.length);
|
||||||
|
_renderGrid(filtered);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Public: open picker ───────────────────────────────────────────────────
|
||||||
|
function open(inputEl) {
|
||||||
|
_targetInput = inputEl;
|
||||||
|
document.getElementById('iconSearchInput').value = '';
|
||||||
|
|
||||||
|
if (!ALL_ICONS.length) {
|
||||||
|
_setStatus('Loading icons...');
|
||||||
|
_renderGrid([]);
|
||||||
|
} else {
|
||||||
|
_setStatus('');
|
||||||
|
_renderGrid(ALL_ICONS);
|
||||||
|
}
|
||||||
|
|
||||||
|
new bootstrap.Modal(document.getElementById('iconPickerModal')).show();
|
||||||
|
}
|
||||||
|
|
||||||
|
function _renderGrid(icons) {
|
||||||
|
const grid = document.getElementById('iconGrid');
|
||||||
|
if (!icons.length) {
|
||||||
|
grid.innerHTML = '<p class="text-muted small">No icons found.</p>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_setStatus(`Showing ${Math.min(icons.length, 200)} / ${icons.length}`);
|
||||||
|
grid.innerHTML = icons.slice(0, 200).map(icon => `
|
||||||
|
<button type="button"
|
||||||
|
class="btn btn-outline-secondary d-flex flex-column align-items-center justify-content-center gap-1"
|
||||||
|
style="width:80px;height:72px;font-size:11px"
|
||||||
|
onclick="IconPickerPick('${_esc(icon.value)}')"
|
||||||
|
title="${_esc(icon.label)}">
|
||||||
|
<i class="${icon.value}" style="font-size:1.4rem"></i>
|
||||||
|
<span class="text-truncate w-100 text-center">${icon.label}</span>
|
||||||
|
</button>
|
||||||
|
`).join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
function pick(value) {
|
||||||
|
if (!_targetInput) return;
|
||||||
|
_targetInput.value = value;
|
||||||
|
|
||||||
|
// Update preview nếu có
|
||||||
|
const previewCell = _targetInput.closest('.input-group')?.querySelector('.icon-preview-cell');
|
||||||
|
if (previewCell) previewCell.innerHTML = `<i class="${value}"></i>`;
|
||||||
|
|
||||||
|
bootstrap.Modal.getInstance(document.getElementById('iconPickerModal')).hide();
|
||||||
|
console.log('[IconPicker] picked:', value);
|
||||||
|
}
|
||||||
|
|
||||||
|
function _setStatus(msg) {
|
||||||
|
const el = document.getElementById('iconPickerStatus');
|
||||||
|
if (el) el.textContent = msg;
|
||||||
|
}
|
||||||
|
|
||||||
|
function _esc(val) {
|
||||||
|
return String(val || '').replace(/"/g, '"').replace(/</g, '<').replace(/>/g, '>');
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Expose global ─────────────────────────────────────────────────────────
|
||||||
|
window.IconPicker = { init, open, pick };
|
||||||
|
window.IconPicker.pick = pick;
|
||||||
|
window.IconPickerPick = pick;
|
||||||
|
})();
|
||||||
@@ -351,8 +351,15 @@
|
|||||||
<div class="row g-2 align-items-center">
|
<div class="row g-2 align-items-center">
|
||||||
<div class="col-md-4">
|
<div class="col-md-4">
|
||||||
<label class="form-label small">Icon (FA class)</label>
|
<label class="form-label small">Icon (FA class)</label>
|
||||||
<input type="text" class="form-control form-control-sm" data-field="icon"
|
<div class="input-group">
|
||||||
value="${esc(item.icon)}" placeholder="e.g. fa-twitter">
|
<span class="input-group-text icon-preview-cell" style="min-width:38px">
|
||||||
|
${item.icon ? `<i class="${esc(item.icon)}"></i>` : ''}
|
||||||
|
</span>
|
||||||
|
<input type="text" class="form-control form-control-sm" data-field="icon"
|
||||||
|
value="${esc(item.icon)}" placeholder="Click to pick..." readonly
|
||||||
|
style="cursor:pointer;background:#fff"
|
||||||
|
onclick="IconPicker.open(this)">
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<label class="form-label small">Href</label>
|
<label class="form-label small">Href</label>
|
||||||
|
|||||||
@@ -804,6 +804,14 @@
|
|||||||
<!-- Toast JS -->
|
<!-- Toast JS -->
|
||||||
<script src="/js/toast.js"></script>
|
<script src="/js/toast.js"></script>
|
||||||
|
|
||||||
|
<!-- Icon Picker -->
|
||||||
|
<script src="/js/icon-picker.js"></script>
|
||||||
|
<script>
|
||||||
|
document.addEventListener('DOMContentLoaded', function () {
|
||||||
|
IconPicker.init();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
<!-- Flash Handler JS -->
|
<!-- Flash Handler JS -->
|
||||||
<script src="/js/flash-handler.js"></script>
|
<script src="/js/flash-handler.js"></script>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user