Enhance mobile cropping experience with dual-mode scanner
- Add toggle between Smart Scan (jscanify) and Manual Crop (cropper.js) modes - Default to Manual Crop mode on touch devices for easier mobile use - Integrate cropper.js with pinch-to-zoom, pan, and touch-friendly handles - Increase corner point size and button sizes for better touch targets - Add mobile help text and visual guidance - Maintain backward compatibility with existing scanner functionality - Improve touch event handling to prevent unwanted scrolling
This commit is contained in:
@@ -5,25 +5,48 @@
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="modalScannerLabel"><i class="fas fa-expand"></i> Smart Scanner</h5>
|
||||
<div class="ml-auto mr-3">
|
||||
<div class="btn-group btn-group-sm" role="group" id="scannerModeToggle">
|
||||
<button type="button" class="btn btn-outline-primary active" data-mode="smart">
|
||||
<i class="fas fa-magic"></i> Smart Scan
|
||||
</button>
|
||||
<button type="button" class="btn btn-outline-secondary" data-mode="manual">
|
||||
<i class="fas fa-crop-alt"></i> Manual Crop
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body text-center bg-dark p-0" style="position: relative; overflow: hidden; height: 80vh;">
|
||||
<!-- Container for Canvases -->
|
||||
<div id="scanner-container" style="position: relative; margin: auto; display: inline-block;">
|
||||
<!-- Smart Scan Mode (Default) -->
|
||||
<div id="scanner-container" class="scanner-mode" data-mode="smart" style="display: block; position: relative; margin: auto; display: inline-block;">
|
||||
<canvas id="canvas-image" style="position: absolute; left: 0; top: 0; z-index: 1;"></canvas>
|
||||
<canvas id="canvas-overlay" style="position: absolute; left: 0; top: 0; z-index: 2; cursor: crosshair;"></canvas>
|
||||
</div>
|
||||
|
||||
<!-- Manual Crop Mode (Cropper.js) -->
|
||||
<div id="crop-container" class="scanner-mode" data-mode="manual" style="display: none; width: 100%; height: 100%;">
|
||||
<img id="crop-image" style="max-width: 100%; max-height: 100%;">
|
||||
</div>
|
||||
|
||||
<div id="scanner-loading" style="position: absolute; top: 50%; left: 50%; transform: translate(-50%,-50%); color: white; display: none;">
|
||||
<i class="fas fa-spinner fa-spin fa-3x"></i><br>Detecting Document...
|
||||
</div>
|
||||
|
||||
<!-- Mobile Help Tips -->
|
||||
<div id="mobile-help" class="d-none d-md-none d-lg-none" style="position: absolute; bottom: 10px; left: 0; right: 0; text-align: center; color: white; background: rgba(0,0,0,0.7); padding: 5px; font-size: 12px;">
|
||||
<span id="help-text-smart">📍 Sentuh & geser titik biru untuk atur sudut (untuk mobile, gunakan Manual Crop)</span>
|
||||
<span id="help-text-manual" style="display: none;">📍 Pinch untuk zoom, geser untuk pindah area crop</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer justify-content-between">
|
||||
<div>
|
||||
<button type="button" class="btn btn-secondary" id="btnScanRotateLeft" title="Putar Kiri (-90°)"><i class="fas fa-undo"></i></button>
|
||||
<button type="button" class="btn btn-secondary" id="btnScanRotateRight" title="Putar Kanan (+90°)"><i class="fas fa-redo"></i></button>
|
||||
<button type="button" class="btn btn-warning" id="btnScanReset"><i class="fas fa-sync"></i> Reset Sudut</button>
|
||||
<button type="button" class="btn btn-info d-none" id="btnCropReset"><i class="fas fa-crop"></i> Reset Crop</button>
|
||||
</div>
|
||||
<div>
|
||||
<button type="button" class="btn btn-secondary" data-dismiss="modal">Batal</button>
|
||||
@@ -142,6 +165,47 @@
|
||||
line-height: 1 !important;
|
||||
margin: -10px -10px -10px auto !important;
|
||||
}
|
||||
|
||||
/* Larger touch targets for all buttons */
|
||||
#modalScanner .modal-footer .btn {
|
||||
min-height: 44px !important;
|
||||
min-width: 44px !important;
|
||||
padding: 10px 15px !important;
|
||||
font-size: 16px !important; /* Prevent zoom on iOS */
|
||||
}
|
||||
|
||||
/* Larger corner points for touch */
|
||||
#canvas-overlay {
|
||||
touch-action: manipulation;
|
||||
}
|
||||
|
||||
/* Mode toggle buttons */
|
||||
#scannerModeToggle .btn {
|
||||
min-height: 36px !important;
|
||||
min-width: 90px !important;
|
||||
font-size: 14px !important;
|
||||
}
|
||||
|
||||
/* Help text */
|
||||
#mobile-help {
|
||||
font-size: 14px !important;
|
||||
padding: 10px !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* Cropper.js customizations for mobile */
|
||||
.cropper-point {
|
||||
width: 30px !important;
|
||||
height: 30px !important;
|
||||
}
|
||||
|
||||
.cropper-line {
|
||||
background-color: rgba(0, 123, 255, 0.5) !important;
|
||||
}
|
||||
|
||||
/* Larger hit area for corner points in smart scan */
|
||||
.scanner-mode[data-mode="smart"] canvas {
|
||||
touch-action: pinch-zoom;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -163,6 +227,10 @@ window.addEventListener('load', function() {
|
||||
}
|
||||
var originalImg = new Image();
|
||||
|
||||
// Cropper.js instance
|
||||
var cropper = null;
|
||||
var currentMode = IS_TOUCH_DEVICE ? 'manual' : 'smart'; // Default to manual for touch devices
|
||||
|
||||
// Corner Points (tl, tr, bl, br)
|
||||
var corners = [];
|
||||
var activePoint = null;
|
||||
@@ -172,15 +240,48 @@ window.addEventListener('load', function() {
|
||||
var touchOffset = null;
|
||||
|
||||
// Config
|
||||
const POINT_RADIUS = 15;
|
||||
const POINT_RADIUS = IS_TOUCH_DEVICE ? 25 : 15; // Larger for touch devices
|
||||
const POINT_COLOR = '#007bff';
|
||||
const LINE_COLOR = '#00ff00';
|
||||
const LINE_WIDTH = 3;
|
||||
const LINE_WIDTH = IS_TOUCH_DEVICE ? 5 : 3;
|
||||
const TOUCH_RADIUS_MULTIPLIER = 2.5; // Larger hit area for touch devices
|
||||
const TOUCH_SLOP = 5; // pixels threshold before dragging starts
|
||||
const IS_TOUCH_DEVICE = 'ontouchstart' in window || navigator.maxTouchPoints > 0;
|
||||
|
||||
// --- Public Function to Open Scanner ---
|
||||
// --- Mode Toggle Handler ---
|
||||
$('#scannerModeToggle button').on('click', function() {
|
||||
var mode = $(this).data('mode');
|
||||
if (mode === currentMode) return;
|
||||
|
||||
// Update UI
|
||||
$('#scannerModeToggle button').removeClass('active btn-primary').addClass('btn-outline-secondary');
|
||||
$(this).removeClass('btn-outline-secondary').addClass('active btn-primary');
|
||||
|
||||
// Switch modes
|
||||
$('.scanner-mode').hide();
|
||||
$('.scanner-mode[data-mode="' + mode + '"]').show();
|
||||
|
||||
// Update help text
|
||||
$('#help-text-smart, #help-text-manual').hide();
|
||||
$('#help-text-' + mode).show();
|
||||
|
||||
// Show/hide appropriate buttons
|
||||
if (mode === 'smart') {
|
||||
$('#btnScanReset').removeClass('d-none');
|
||||
$('#btnCropReset').addClass('d-none');
|
||||
} else {
|
||||
$('#btnScanReset').addClass('d-none');
|
||||
$('#btnCropReset').removeClass('d-none');
|
||||
|
||||
// Initialize cropper if not already
|
||||
if (typeof Cropper !== 'undefined' && !cropper && originalImg.src) {
|
||||
initCropper();
|
||||
}
|
||||
}
|
||||
|
||||
currentMode = mode;
|
||||
});
|
||||
|
||||
// --- Public Function to Open Scanner ---
|
||||
window.openScanner = function(file) {
|
||||
if (!file) return;
|
||||
@@ -232,8 +333,14 @@ window.addEventListener('load', function() {
|
||||
var modalFooter = document.querySelector('#modalScanner .modal-footer');
|
||||
|
||||
var preventTouch = function(e) {
|
||||
// Only prevent if not clicking a button
|
||||
if (e.target.tagName !== 'BUTTON' && !e.target.closest('button')) {
|
||||
// Allow touch on canvas elements and buttons
|
||||
var target = e.target;
|
||||
var isCanvas = target.id === 'canvas-overlay' || target.id === 'canvas-image' ||
|
||||
target.id === 'crop-image' || target.closest('#crop-container') ||
|
||||
target.closest('#scanner-container');
|
||||
var isButton = target.tagName === 'BUTTON' || target.closest('button');
|
||||
|
||||
if (!isCanvas && !isButton) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}
|
||||
@@ -262,6 +369,8 @@ window.addEventListener('load', function() {
|
||||
document.body.style.overflow = '';
|
||||
document.body.style.position = '';
|
||||
document.body.style.width = '';
|
||||
// Cleanup cropper
|
||||
destroyCropper();
|
||||
});
|
||||
|
||||
scannerModal.modal('show');
|
||||
@@ -346,6 +455,81 @@ window.addEventListener('load', function() {
|
||||
|
||||
detectDocument(scale, w, h, false);
|
||||
$('#scanner-loading').hide();
|
||||
|
||||
// Prepare image for cropper
|
||||
var cropImage = document.getElementById('crop-image');
|
||||
cropImage.src = originalImg.src;
|
||||
|
||||
// Initialize cropper if in manual mode
|
||||
if (currentMode === 'manual' && typeof Cropper !== 'undefined') {
|
||||
initCropper();
|
||||
}
|
||||
}
|
||||
|
||||
function initCropper() {
|
||||
if (typeof Cropper === 'undefined') {
|
||||
console.error('Cropper.js not loaded');
|
||||
alert('Cropper library not loaded. Please refresh the page.');
|
||||
return;
|
||||
}
|
||||
|
||||
var cropImage = document.getElementById('crop-image');
|
||||
if (!cropImage.src) {
|
||||
console.warn('Crop image not loaded yet');
|
||||
return;
|
||||
}
|
||||
|
||||
// Destroy existing cropper
|
||||
if (cropper) {
|
||||
cropper.destroy();
|
||||
cropper = null;
|
||||
}
|
||||
|
||||
// Initialize new cropper with mobile-friendly options
|
||||
cropper = new Cropper(cropImage, {
|
||||
viewMode: 1,
|
||||
dragMode: 'crop',
|
||||
initialAspectRatio: 16 / 9,
|
||||
aspectRatio: NaN, // Free aspect ratio
|
||||
autoCrop: true,
|
||||
autoCropArea: 0.8,
|
||||
responsive: true,
|
||||
restore: true,
|
||||
checkCrossOrigin: false,
|
||||
highlight: false,
|
||||
cropBoxMovable: true,
|
||||
cropBoxResizable: true,
|
||||
toggleDragModeOnDblclick: false,
|
||||
minCanvasWidth: 100,
|
||||
minCanvasHeight: 100,
|
||||
minContainerWidth: 100,
|
||||
minContainerHeight: 100,
|
||||
minCropBoxWidth: 50,
|
||||
minCropBoxHeight: 50,
|
||||
|
||||
// Mobile touch settings
|
||||
touchDragZoom: true,
|
||||
wheelZoomRatio: 0.1,
|
||||
|
||||
ready: function() {
|
||||
// Adjust for mobile
|
||||
if (IS_TOUCH_DEVICE) {
|
||||
// Make handles larger for touch
|
||||
var points = this.cropper.cropBox.querySelectorAll('.cropper-point');
|
||||
points.forEach(function(point) {
|
||||
point.style.width = '30px';
|
||||
point.style.height = '30px';
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function destroyCropper() {
|
||||
if (cropper) {
|
||||
cropper.destroy();
|
||||
cropper = null;
|
||||
}
|
||||
}
|
||||
|
||||
function defaultCorners(w, h) {
|
||||
@@ -524,7 +708,20 @@ window.addEventListener('load', function() {
|
||||
// Update originalImg
|
||||
var rotatedUrl = offCanvas.toDataURL();
|
||||
originalImg.onload = function() {
|
||||
initScanner(); // Re-init with new image
|
||||
// Re-init scanner for both modes
|
||||
if (currentMode === 'smart') {
|
||||
initScanner();
|
||||
} else {
|
||||
// Update cropper image
|
||||
var cropImage = document.getElementById('crop-image');
|
||||
cropImage.src = rotatedUrl;
|
||||
|
||||
// Reinitialize cropper
|
||||
if (cropper) {
|
||||
cropper.destroy();
|
||||
}
|
||||
initCropper();
|
||||
}
|
||||
}
|
||||
originalImg.src = rotatedUrl;
|
||||
}
|
||||
@@ -537,10 +734,20 @@ window.addEventListener('load', function() {
|
||||
initScanner();
|
||||
});
|
||||
|
||||
// --- Crop Reset Button ---
|
||||
$('#btnCropReset').click(function() {
|
||||
if (cropper) {
|
||||
cropper.reset();
|
||||
}
|
||||
});
|
||||
|
||||
// --- Save / Extract Button ---
|
||||
$('#btnScanSave').click(function() {
|
||||
// Warp Image
|
||||
try {
|
||||
var base64;
|
||||
|
||||
if (currentMode === 'smart') {
|
||||
// Smart Scan Mode - Use jscanify
|
||||
// 1. Get raw points relative to Original Image
|
||||
var scaleX = originalImg.width / canvasImage.width;
|
||||
var scaleY = originalImg.height / canvasImage.height;
|
||||
@@ -574,7 +781,26 @@ window.addEventListener('load', function() {
|
||||
alert("Scanner library not loaded. Please refresh the page.");
|
||||
return;
|
||||
}
|
||||
var base64 = resultCanvas.toDataURL('image/jpeg');
|
||||
base64 = resultCanvas.toDataURL('image/jpeg');
|
||||
|
||||
} else {
|
||||
// Manual Crop Mode - Use cropper.js
|
||||
if (!cropper) {
|
||||
alert("Cropper not initialized. Please try again.");
|
||||
return;
|
||||
}
|
||||
|
||||
// Get cropped canvas
|
||||
var canvas = cropper.getCroppedCanvas({
|
||||
width: originalImg.width,
|
||||
height: originalImg.height,
|
||||
fillColor: '#fff',
|
||||
imageSmoothingEnabled: true,
|
||||
imageSmoothingQuality: 'high'
|
||||
});
|
||||
|
||||
base64 = canvas.toDataURL('image/jpeg');
|
||||
}
|
||||
|
||||
if (window.handleScannerResult) {
|
||||
window.handleScannerResult(base64);
|
||||
@@ -584,8 +810,23 @@ window.addEventListener('load', function() {
|
||||
|
||||
} catch (e) {
|
||||
alert("Gagal memproses gambar: " + e.message);
|
||||
console.error(e);
|
||||
}
|
||||
});
|
||||
|
||||
// Show mobile help if touch device
|
||||
if (IS_TOUCH_DEVICE) {
|
||||
$('#mobile-help').removeClass('d-none');
|
||||
}
|
||||
|
||||
// Set initial mode based on device
|
||||
setTimeout(function() {
|
||||
var initialMode = IS_TOUCH_DEVICE ? 'manual' : 'smart';
|
||||
var button = $('#scannerModeToggle button[data-mode="' + initialMode + '"]');
|
||||
if (button.length) {
|
||||
button.click();
|
||||
}
|
||||
}, 100);
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
Reference in New Issue
Block a user