Fase 3 feedback round: style preview, icon upload, font choice, optional bitcoin amount, WPA3
- Default the Owner select to the creator's own account instead of "All", so
superadmin-created codes no longer silently become visible to every admin
(the underlying NULL-fallback sharing behavior for an explicit "All" choice
is unchanged).
- Add a live color/precision/size preview swatch next to the preset picker.
- Let the frame text use a chosen DejaVu font + font size instead of a fixed
GD bitmap font.
- Add an optional self-uploaded icon rendered above the qr code (not embedded
in it, so scanability is unaffected).
- Make the Bitcoin qr amount optional; a standing wallet address is useful
without forcing a one-off amount per code.
- Add a WPA3 option to the WiFi qr encryption select.
- Fix a real bug surfaced while testing the preview/style JS: qrcode_options.php
was included once per static qr type (16 times on one page) and each
inclusion re-executed <script src="qrcode-style-tools.js">, so every button
click fired once per type - e.g. saving one preset wrote 16 duplicate rows,
and every tab except the first ("Text") had dead random-style/preset
buttons since only the first DOM match ever got a listener. Moved the
script include to load once per page and rewrote the JS to scope every
lookup to the triggering element's own tab-pane/form instead of relying on
getElementById's first-match behavior.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0196bLhZhuxwK3MkHuKLe5WB
This commit is contained in:
Vendored
+121
-41
@@ -1,12 +1,23 @@
|
||||
// Preset system + random style button for the qr code generation forms (Fase 3, priority 2).
|
||||
//
|
||||
// The static qr "add" page stacks all qr-type forms (text/email/.../wifi/bitcoin/...) into
|
||||
// the DOM at once as Bootstrap tab-panes, and every one of them repeats the same element ids
|
||||
// (foreground, background, size, random_style_btn, ...). getElementById/getElementsBy* only
|
||||
// ever finds the *first* of those (the "Text" tab), so every helper below is scoped to the
|
||||
// specific tab-pane/form the triggering element lives in, and wired up via querySelectorAll
|
||||
// so every tab gets working listeners, not just the first one.
|
||||
(function () {
|
||||
function csrfToken() {
|
||||
var meta = document.querySelector('meta[name="csrf-token"]');
|
||||
return meta ? meta.getAttribute('content') : '';
|
||||
}
|
||||
|
||||
function setColor(id, hex) {
|
||||
var input = document.getElementById(id);
|
||||
function scopeOf(el) {
|
||||
return el.closest('.tab-pane') || el.closest('form') || document;
|
||||
}
|
||||
|
||||
function setColor(scope, id, hex) {
|
||||
var input = scope.querySelector('#' + id);
|
||||
if (!input) {
|
||||
return;
|
||||
}
|
||||
@@ -27,12 +38,35 @@
|
||||
return '#' + ('000000' + value).slice(-6);
|
||||
}
|
||||
|
||||
function loadPresets() {
|
||||
var select = document.getElementById('preset_select');
|
||||
if (!select) {
|
||||
function updateStylePreview(scope) {
|
||||
var swatch = scope.querySelector('#style_preview_swatch');
|
||||
var text = scope.querySelector('#style_preview_text');
|
||||
if (!swatch || !text) {
|
||||
return;
|
||||
}
|
||||
|
||||
var foreground = scope.querySelector('#foreground');
|
||||
var background = scope.querySelector('#background');
|
||||
var levelSelect = scope.querySelector('select[name="level"]');
|
||||
var sizeSelect = scope.querySelector('#size');
|
||||
|
||||
var fg = foreground ? foreground.value : '#000000';
|
||||
var bg = background ? background.value : '#ffffff';
|
||||
|
||||
swatch.style.background = bg;
|
||||
swatch.style.borderColor = fg;
|
||||
|
||||
var parts = [];
|
||||
if (levelSelect) {
|
||||
parts.push('Precision: ' + levelSelect.value);
|
||||
}
|
||||
if (sizeSelect) {
|
||||
parts.push('Size: ' + sizeSelect.value + 'px');
|
||||
}
|
||||
text.textContent = parts.join(' · ');
|
||||
}
|
||||
|
||||
function loadPresetsInto(select) {
|
||||
fetch('presets.php?action=list')
|
||||
.then(function (response) { return response.json(); })
|
||||
.then(function (json) {
|
||||
@@ -54,43 +88,69 @@
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
loadPresets();
|
||||
document.querySelectorAll('#preset_select').forEach(loadPresetsInto);
|
||||
|
||||
var randomBtn = document.getElementById('random_style_btn');
|
||||
if (randomBtn) {
|
||||
randomBtn.addEventListener('click', function () {
|
||||
setColor('foreground', randomHexColor());
|
||||
setColor('background', randomHexColor());
|
||||
document.querySelectorAll('#style_preview').forEach(function (row) {
|
||||
updateStylePreview(scopeOf(row));
|
||||
});
|
||||
|
||||
document.querySelectorAll('#foreground, #background').forEach(function (input) {
|
||||
input.addEventListener('change', function () {
|
||||
updateStylePreview(scopeOf(input));
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
var presetSelect = document.getElementById('preset_select');
|
||||
if (presetSelect) {
|
||||
document.querySelectorAll('select[name="level"]').forEach(function (select) {
|
||||
select.addEventListener('change', function () {
|
||||
updateStylePreview(scopeOf(select));
|
||||
});
|
||||
});
|
||||
|
||||
document.querySelectorAll('#size').forEach(function (select) {
|
||||
select.addEventListener('change', function () {
|
||||
updateStylePreview(scopeOf(select));
|
||||
});
|
||||
});
|
||||
|
||||
document.querySelectorAll('#random_style_btn').forEach(function (randomBtn) {
|
||||
randomBtn.addEventListener('click', function () {
|
||||
var scope = scopeOf(randomBtn);
|
||||
setColor(scope, 'foreground', randomHexColor());
|
||||
setColor(scope, 'background', randomHexColor());
|
||||
updateStylePreview(scope);
|
||||
});
|
||||
});
|
||||
|
||||
document.querySelectorAll('#preset_select').forEach(function (presetSelect) {
|
||||
presetSelect.addEventListener('change', function () {
|
||||
var option = presetSelect.options[presetSelect.selectedIndex];
|
||||
if (!option.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
setColor('foreground', option.dataset.foreground);
|
||||
setColor('background', option.dataset.background);
|
||||
var scope = scopeOf(presetSelect);
|
||||
|
||||
var levelSelect = document.querySelector('select[name="level"]');
|
||||
setColor(scope, 'foreground', option.dataset.foreground);
|
||||
setColor(scope, 'background', option.dataset.background);
|
||||
|
||||
var levelSelect = scope.querySelector('select[name="level"]');
|
||||
if (levelSelect) {
|
||||
levelSelect.value = option.dataset.level;
|
||||
}
|
||||
|
||||
var sizeSelect = document.getElementById('size');
|
||||
var sizeSelect = scope.querySelector('#size');
|
||||
if (sizeSelect) {
|
||||
sizeSelect.value = option.dataset.size;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
var saveBtn = document.getElementById('preset_save_btn');
|
||||
if (saveBtn) {
|
||||
updateStylePreview(scope);
|
||||
});
|
||||
});
|
||||
|
||||
document.querySelectorAll('#preset_save_btn').forEach(function (saveBtn) {
|
||||
saveBtn.addEventListener('click', function () {
|
||||
var nameInput = document.getElementById('preset_name');
|
||||
var scope = scopeOf(saveBtn);
|
||||
var nameInput = scope.querySelector('#preset_name');
|
||||
var name = nameInput.value.trim();
|
||||
|
||||
if (!name) {
|
||||
@@ -98,13 +158,18 @@
|
||||
return;
|
||||
}
|
||||
|
||||
var foreground = scope.querySelector('#foreground').value;
|
||||
var background = scope.querySelector('#background').value;
|
||||
var level = scope.querySelector('select[name="level"]').value;
|
||||
var size = scope.querySelector('#size').value;
|
||||
|
||||
var body = new URLSearchParams();
|
||||
body.set('action', 'save');
|
||||
body.set('name', name);
|
||||
body.set('foreground', document.getElementById('foreground').value);
|
||||
body.set('background', document.getElementById('background').value);
|
||||
body.set('level', document.querySelector('select[name="level"]').value);
|
||||
body.set('size', document.getElementById('size').value);
|
||||
body.set('foreground', foreground);
|
||||
body.set('background', background);
|
||||
body.set('level', level);
|
||||
body.set('size', size);
|
||||
|
||||
fetch('presets.php', {
|
||||
method: 'POST',
|
||||
@@ -118,24 +183,33 @@
|
||||
return;
|
||||
}
|
||||
|
||||
var option = document.createElement('option');
|
||||
option.value = json.data.id;
|
||||
option.textContent = json.data.name;
|
||||
option.dataset.foreground = document.getElementById('foreground').value;
|
||||
option.dataset.background = document.getElementById('background').value;
|
||||
option.dataset.level = document.querySelector('select[name="level"]').value;
|
||||
option.dataset.size = document.getElementById('size').value;
|
||||
presetSelect.appendChild(option);
|
||||
presetSelect.value = option.value;
|
||||
// The preset list is shared across every tab, so mirror the new
|
||||
// option into every preset_select on the page, not just this one.
|
||||
document.querySelectorAll('#preset_select').forEach(function (select) {
|
||||
var option = document.createElement('option');
|
||||
option.value = json.data.id;
|
||||
option.textContent = json.data.name;
|
||||
option.dataset.foreground = foreground;
|
||||
option.dataset.background = background;
|
||||
option.dataset.level = level;
|
||||
option.dataset.size = size;
|
||||
select.appendChild(option);
|
||||
|
||||
if (select === scope.querySelector('#preset_select')) {
|
||||
select.value = option.value;
|
||||
}
|
||||
});
|
||||
|
||||
nameInput.value = '';
|
||||
})
|
||||
.catch(function () { alert('Could not save preset (network error).'); });
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
var deleteBtn = document.getElementById('preset_delete_btn');
|
||||
if (deleteBtn) {
|
||||
document.querySelectorAll('#preset_delete_btn').forEach(function (deleteBtn) {
|
||||
deleteBtn.addEventListener('click', function () {
|
||||
var scope = scopeOf(deleteBtn);
|
||||
var presetSelect = scope.querySelector('#preset_select');
|
||||
var option = presetSelect.options[presetSelect.selectedIndex];
|
||||
if (!option.value) {
|
||||
return;
|
||||
@@ -157,13 +231,19 @@
|
||||
.then(function (response) { return response.json(); })
|
||||
.then(function (json) {
|
||||
if (json.status === 200) {
|
||||
option.remove();
|
||||
// Remove the matching option from every preset_select on the page.
|
||||
document.querySelectorAll('#preset_select').forEach(function (select) {
|
||||
var match = select.querySelector('option[value="' + option.value + '"]');
|
||||
if (match) {
|
||||
match.remove();
|
||||
}
|
||||
});
|
||||
} else {
|
||||
alert('Could not delete preset: ' + json.data);
|
||||
}
|
||||
})
|
||||
.catch(function () { alert('Could not delete preset (network error).'); });
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
})();
|
||||
|
||||
Reference in New Issue
Block a user