diff --git a/lab_formulario_builder.php b/lab_formulario_builder.php index ea059ca..46ad85b 100644 --- a/lab_formulario_builder.php +++ b/lab_formulario_builder.php @@ -215,10 +215,47 @@ $formId = isset($_GET['id']) ? (int)$_GET['id'] : 0; ::-webkit-scrollbar-thumb { background: #c1c9d4; border-radius: 3px; } ::-webkit-scrollbar-thumb:hover { background: #8fa0b5; } + /* ── Drop indicators (touch drag) ─────────────────────── */ + .field-item.touch-dragging { opacity: .3; } + .field-item.drop-above::before { + content: ''; position: absolute; top: -4px; left: 0; right: 0; + height: 3px; background: #1565c0; border-radius: 2px; pointer-events: none; + } + .field-item.drop-below::after { + content: ''; position: absolute; bottom: -4px; left: 0; right: 0; + height: 3px; background: #1565c0; border-radius: 2px; pointer-events: none; + } + #drag-ghost { + position: fixed; pointer-events: none; z-index: 9999; + background: #fff; border: 1.5px solid #1565c0; border-radius: 8px; + box-shadow: 0 8px 28px rgba(21,101,192,.25); + padding: 9px 12px; display: flex; align-items: center; gap: 8px; + font-size: 12.5px; font-weight: 600; color: #1565c0; + opacity: .92; transition: none; + } + + /* ── Palette tap hint ──────────────────────────────────── */ + .palette-item { touch-action: none; } + @media (pointer: coarse) { + .palette-item::after { + content: '+'; margin-left: auto; font-size: 15px; + font-weight: 700; color: #1565c0; opacity: .5; + } + .field-item .fi-handle { + font-size: 18px; padding: 4px 6px; + color: #90a4ae; + } + .field-item .fi-btn { padding: 6px 8px; font-size: 14px; } + } + @media (max-width: 900px) { #panel-palette { width: 170px; min-width: 170px; } #panel-preview { width: 260px; min-width: 260px; } } + @media (max-width: 680px) { + #panel-preview { display: none; } + #panel-palette { width: 140px; min-width: 140px; } + } /* ── Pantalla de éxito ─────────────────────────────── */ #success-screen { @@ -530,14 +567,16 @@ function renderPalette() { $('palette').innerHTML = TIPOS_CAMPO.map(t => `
+ ondragstart="palDragStart(event,'${t.tipo}')" + onclick="agregarCampo({tipo:'${t.tipo}',linked:null})"> ${t.label}
`).join(''); $('palette-linked').innerHTML = TIPOS_LINKED.map(t => `
+ ondragstart="palDragStart(event,'linked','${t.key}')" + onclick="agregarCampo({tipo:'linked',linked:'${t.key}'})" ${t.label}
`).join(''); @@ -611,7 +650,8 @@ const TIPO_ICON = { texto:'fa-font', textarea:'fa-align-left', numero:'fa-hashtag', fecha:'fa-calendar', hora:'fa-clock', select:'fa-list', radio:'fa-dot-circle', checkbox:'fa-check-square', firma:'fa-signature', firma_profesional:'fa-user-md', separador:'fa-minus', linked:'fa-link', - parrafo:'fa-paragraph', parrafo_inline:'fa-align-left', lista_marcable:'fa-list-ol' + parrafo:'fa-paragraph', parrafo_inline:'fa-align-left', lista_marcable:'fa-list-ol', + calculado:'fa-calculator' }; function renderCanvas() { @@ -648,29 +688,139 @@ function renderCanvas() { `; - // Reordenar drag + // Reordenar — HTML5 drag (mouse desktop) + div.setAttribute('draggable', 'true'); div.addEventListener('dragstart', e => { + if (e.target.closest('.fi-btn')) { e.preventDefault(); return; } _dragSrc = idx; _dragTipo = null; e.dataTransfer.effectAllowed = 'move'; div.classList.add('dragging'); }); - div.addEventListener('dragend', () => div.classList.remove('dragging')); - div.addEventListener('dragover', e => { e.preventDefault(); e.dataTransfer.dropEffect='move'; }); + div.addEventListener('dragend', () => { + div.classList.remove('dragging'); + $('drop-zone').querySelectorAll('.field-item').forEach(fi => + fi.classList.remove('drop-above','drop-below')); + }); + div.addEventListener('dragover', e => { + e.preventDefault(); e.dataTransfer.dropEffect = 'move'; + $('drop-zone').querySelectorAll('.field-item').forEach(fi => + fi.classList.remove('drop-above','drop-below')); + const r = div.getBoundingClientRect(); + div.classList.add(e.clientY < r.top + r.height / 2 ? 'drop-above' : 'drop-below'); + }); div.addEventListener('drop', e => { e.stopPropagation(); + $('drop-zone').querySelectorAll('.field-item').forEach(fi => + fi.classList.remove('drop-above','drop-below')); if (_dragSrc !== null && _dragSrc !== idx) { + const r = div.getBoundingClientRect(); + const before = e.clientY < r.top + r.height / 2; const [moved] = _campos.splice(_dragSrc, 1); - _campos.splice(idx, 0, moved); + const dest = _dragSrc < idx ? (before ? idx - 1 : idx) : (before ? idx : idx + 1); + _campos.splice(dest, 0, moved); _dragSrc = null; - renderCanvas(); - renderPreview(); + renderCanvas(); renderPreview(); } }); + // Reordenar — Pointer Events (touch / tablet) + initPointerDrag(div, idx); zone.appendChild(div); }); } +// ════════════════════════════════════════════════════════════════════ +// POINTER DRAG (touch + tablet) +// ════════════════════════════════════════════════════════════════════ +let _pDrag = null; + +function initPointerDrag(div, idx) { + const handle = div.querySelector('.fi-handle'); + if (!handle) return; + + handle.addEventListener('pointerdown', e => { + // Solo si es touch o el botón primario del mouse (pero el mouse usa HTML5) + if (e.pointerType === 'mouse') return; + e.preventDefault(); + e.stopPropagation(); + + _dragSrc = idx; _dragTipo = null; + const rect = div.getBoundingClientRect(); + + // Crear ghost + const ghost = document.createElement('div'); + ghost.id = 'drag-ghost'; + ghost.innerHTML = div.querySelector('.fi-icon').outerHTML + + '' + (div.querySelector('.fi-label')?.textContent || '') + ''; + ghost.style.top = rect.top + 'px'; + ghost.style.left = rect.left + 'px'; + ghost.style.width = rect.width + 'px'; + document.body.appendChild(ghost); + + div.classList.add('touch-dragging'); + handle.setPointerCapture(e.pointerId); + + _pDrag = { idx, div, ghost, offsetY: e.clientY - rect.top }; + }); + + handle.addEventListener('pointermove', e => { + if (!_pDrag || _pDrag.idx !== idx) return; + e.preventDefault(); + const { ghost } = _pDrag; + ghost.style.top = (e.clientY - _pDrag.offsetY) + 'px'; + + // Indicador de posición + $('drop-zone').querySelectorAll('.field-item').forEach(fi => + fi.classList.remove('drop-above','drop-below')); + ghost.style.display = 'none'; + const el = document.elementFromPoint(e.clientX, e.clientY); + ghost.style.display = ''; + const target = el?.closest('.field-item'); + if (target && target !== _pDrag.div) { + const r = target.getBoundingClientRect(); + target.classList.add(e.clientY < r.top + r.height / 2 ? 'drop-above' : 'drop-below'); + } + }); + + const endDrag = e => { + if (!_pDrag || _pDrag.idx !== idx) return; + const { div: origDiv, ghost } = _pDrag; + + ghost.style.display = 'none'; + const el = document.elementFromPoint(e.clientX, e.clientY); + ghost.remove(); + origDiv.classList.remove('touch-dragging'); + $('drop-zone').querySelectorAll('.field-item').forEach(fi => + fi.classList.remove('drop-above','drop-below')); + + const target = el?.closest('.field-item'); + if (target && target !== origDiv) { + const targetIdx = parseInt(target.dataset.idx); + const r = target.getBoundingClientRect(); + const before = e.clientY < r.top + r.height / 2; + if (!isNaN(targetIdx) && targetIdx !== idx) { + const [moved] = _campos.splice(idx, 1); + const dest = idx < targetIdx + ? (before ? targetIdx - 1 : targetIdx) + : (before ? targetIdx : targetIdx + 1); + _campos.splice(dest, 0, moved); + renderCanvas(); renderPreview(); + } + } + _pDrag = null; _dragSrc = null; + }; + + handle.addEventListener('pointerup', endDrag); + handle.addEventListener('pointercancel', e => { + if (!_pDrag || _pDrag.idx !== idx) return; + _pDrag.ghost.remove(); + _pDrag.div.classList.remove('touch-dragging'); + $('drop-zone').querySelectorAll('.field-item').forEach(fi => + fi.classList.remove('drop-above','drop-below')); + _pDrag = null; _dragSrc = null; + }); +} + function eliminarCampo(idx) { _campos.splice(idx, 1); renderCanvas();