Fix dropdown behavior.

This commit is contained in:
Yaro Kasear 2025-12-31 08:48:35 -06:00
parent d2061f5c1c
commit 6f175c103e
3 changed files with 68 additions and 23 deletions

View file

@ -25,7 +25,7 @@
} }
.grid-widget [data-canvas] { .grid-widget [data-canvas] {
z-index: 9999; z-index: 1;
pointer-events: none; pointer-events: none;
inset: 0; inset: 0;
} }
@ -38,7 +38,7 @@
.grid-widget [data-dot] { .grid-widget [data-dot] {
transform: translate(-50%, -50%); transform: translate(-50%, -50%);
z-index: 10000; z-index: 2;
pointer-events: none; pointer-events: none;
} }
@ -46,7 +46,7 @@
display: flex; display: flex;
flex-wrap: nowrap; flex-wrap: nowrap;
overflow-x: auto; overflow-x: auto;
overflow-y: hidden; overflow-y: visible;
gap: .5rem; gap: .5rem;
align-items: center; align-items: center;
} }
@ -57,6 +57,26 @@
flex: 0 0 120px; flex: 0 0 120px;
} }
.grid-widget .dropdown-menu {
z-index: 2000;
min-width: 220px;
padding: .5rem .75rem;
border-radius: .5rem;
}
.grid-widget .dropdown-menu > *:first-child {
margin-top: 0;
}
.grid-widget .dropdown-menu > *:last-child {
margin-bottom: 0;
}
.grid-widget .dropdown-menu .form-range {
width: 100%;
margin: 0;
}
.grid-widget [data-toolbar] .badge, .grid-widget [data-toolbar] .badge,
.grid-widget [data-toolbar] .input-group-text { .grid-widget [data-toolbar] .input-group-text {
white-space: nowrap; white-space: nowrap;

View file

@ -14,6 +14,11 @@ document.addEventListener('keydown', (e) => {
function initGridWidget(root, opts = {}) { function initGridWidget(root, opts = {}) {
const DEFAULT_DOC = { version: 1, cellSize: 25, shapes: [] }; const DEFAULT_DOC = { version: 1, cellSize: 25, shapes: [] };
const MAX_HISTORY = 100; const MAX_HISTORY = 100;
const SHAPE_DEFAULTS = {
strokeWidth: 0.12,
strokeOpacity: 1,
fillOpacity: 1
};
const canvasEl = root.querySelector('[data-canvas]'); const canvasEl = root.querySelector('[data-canvas]');
const clearEl = root.querySelector('[data-clear]'); const clearEl = root.querySelector('[data-clear]');
@ -39,7 +44,7 @@ function initGridWidget(root, opts = {}) {
if (cellSizeEl && cellSizeValEl) bindRangeWithLabel(cellSizeEl, cellSizeValEl, v => `${v}px`); if (cellSizeEl && cellSizeValEl) bindRangeWithLabel(cellSizeEl, cellSizeValEl, v => `${v}px`);
if (fillOpacityEl && fillValEl) bindRangeWithLabel(fillOpacityEl, fillValEl, v => `${parseInt(Number(v) * 100)}%`); if (fillOpacityEl && fillValEl) bindRangeWithLabel(fillOpacityEl, fillValEl, v => `${parseInt(Number(v) * 100)}%`);
if (strokeOpacityEl && strokeValEl) bindRangeWithLabel(strokeOpacityEl, strokeValEl, v => `${parseInt(Number(v) * 100)}%`); if (strokeOpacityEl && strokeValEl) bindRangeWithLabel(strokeOpacityEl, strokeValEl, v => `${parseInt(Number(v) * 100)}%`);
if (strokeWidthEl && widthValEl) bindRangeWithLabel(strokeWidthEl, widthValEl, v => `${parseInt(Number(v) * 100)}%`); if (strokeWidthEl && widthValEl) bindRangeWithLabel(strokeWidthEl, widthValEl, v => `${parseInt(Number(v) * cellSizeEl.value)}p×`);
const storageKey = opts.storageKey ?? 'gridDoc'; const storageKey = opts.storageKey ?? 'gridDoc';
@ -81,6 +86,22 @@ function initGridWidget(root, opts = {}) {
let activePointerId = null; let activePointerId = null;
toolBarEl.querySelectorAll('[data-bs-toggle="dropdown"]').forEach((toggle) => {
bootstrap.Dropdown.getOrCreateInstance(toggle, {
popperConfig(defaultConfig) {
return {
...defaultConfig,
strategy: 'fixed',
modifiers: [
...(defaultConfig.modifiers || []),
{ name: 'preventOverflow', options: { boundary: 'viewport' } },
{ name: 'flip', options: { boundary: 'viewport', padding: 8 } },
],
};
},
});
});
const api = { const api = {
handleKeyDown(e) { handleKeyDown(e) {
const key = e.key.toLowerCase(); const key = e.key.toLowerCase();
@ -224,8 +245,8 @@ function initGridWidget(root, opts = {}) {
if (!s || typeof s !== 'object' || !allowed.has(s.type)) return []; if (!s || typeof s !== 'object' || !allowed.has(s.type)) return [];
const color = typeof s.color === 'string' ? s.color : '#000000'; const color = typeof s.color === 'string' ? s.color : '#000000';
const fillOpacity = clamp01(s.fillOpacity, 1); const fillOpacity = clamp01(s.fillOpacity, SHAPE_DEFAULTS.fillOpacity);
const strokeOpacity = clamp01(s.strokeOpacity, 1); const strokeOpacity = clamp01(s.strokeOpacity, SHAPE_DEFAULTS.strokeOpacity);
if (s.type === 'line') { if (s.type === 'line') {
if (!['x1', 'y1', 'x2', 'y2'].every(k => isFiniteNum(s[k]))) return []; if (!['x1', 'y1', 'x2', 'y2'].every(k => isFiniteNum(s[k]))) return [];
@ -233,7 +254,7 @@ function initGridWidget(root, opts = {}) {
type: 'line', type: 'line',
x1: +s.x1, y1: +s.y1, x2: +s.x2, y2: +s.y2, x1: +s.x1, y1: +s.y1, x2: +s.x2, y2: +s.y2,
color, color,
strokeWidth: normStroke(s.strokeWidth), strokeWidth: normStroke(s.strokeWidth, SHAPE_DEFAULTS.strokeWidth),
strokeOpacity strokeOpacity
}]; }];
} }
@ -250,7 +271,7 @@ function initGridWidget(root, opts = {}) {
type: 'path', type: 'path',
points, points,
color, color,
strokeWidth: normStroke(s.strokeWidth, 0.12), strokeWidth: normStroke(s.strokeWidth, SHAPE_DEFAULTS.strokeWidth),
strokeOpacity strokeOpacity
}]; }];
} }
@ -263,7 +284,7 @@ function initGridWidget(root, opts = {}) {
fill: !!s.fill, fill: !!s.fill,
fillOpacity, fillOpacity,
strokeOpacity, strokeOpacity,
strokeWidth: normStroke(s.strokeWidth) strokeWidth: normStroke(s.strokeWidth, SHAPE_DEFAULTS.strokeWidth)
}]; }];
}); });
} }
@ -473,7 +494,7 @@ function initGridWidget(root, opts = {}) {
ctx.save(); ctx.save();
ctx.strokeStyle = shape.color || '#000000'; ctx.strokeStyle = shape.color || '#000000';
ctx.lineWidth = Math.max(1, toPx(shape.strokeWidth ?? 0.12)); ctx.lineWidth = Math.max(1, toPx(shape.strokeWidth ?? SHAPE_DEFAULTS.strokeWidth));
if (shape.type === 'rect' || shape.type === 'ellipse') { if (shape.type === 'rect' || shape.type === 'ellipse') {
const x = toPx(shape.x); const x = toPx(shape.x);
@ -481,7 +502,7 @@ function initGridWidget(root, opts = {}) {
const w = toPx(shape.w); const w = toPx(shape.w);
const h = toPx(shape.h); const h = toPx(shape.h);
ctx.globalAlpha = clamp01(shape.strokeOpacity, 1); ctx.globalAlpha = clamp01(shape.strokeOpacity, SHAPE_DEFAULTS.strokeOpacity);
if (shape.type === 'rect') { if (shape.type === 'rect') {
ctx.strokeRect(x, y, w, h); ctx.strokeRect(x, y, w, h);
} else { } else {
@ -494,7 +515,7 @@ function initGridWidget(root, opts = {}) {
ctx.globalAlpha = 1; ctx.globalAlpha = 1;
if (shape.fill) { if (shape.fill) {
ctx.globalAlpha = clamp01(shape.fillOpacity, 1); ctx.globalAlpha = clamp01(shape.fillOpacity, SHAPE_DEFAULTS.fillOpacity);
ctx.fillStyle = shape.color; ctx.fillStyle = shape.color;
if (shape.type === 'rect') { if (shape.type === 'rect') {
ctx.fillRect(x, y, w, h); ctx.fillRect(x, y, w, h);
@ -514,7 +535,7 @@ function initGridWidget(root, opts = {}) {
const x2 = toPx(shape.x2); const x2 = toPx(shape.x2);
const y2 = toPx(shape.y2); const y2 = toPx(shape.y2);
ctx.globalAlpha = clamp01(shape.strokeOpacity, 1); ctx.globalAlpha = clamp01(shape.strokeOpacity, SHAPE_DEFAULTS.strokeOpacity);
ctx.beginPath(); ctx.beginPath();
ctx.moveTo(x1, y1); ctx.moveTo(x1, y1);
ctx.lineTo(x2, y2); ctx.lineTo(x2, y2);
@ -523,9 +544,9 @@ function initGridWidget(root, opts = {}) {
} else if (shape.type === 'path') { } else if (shape.type === 'path') {
const toPx = (v) => v * cellSize; const toPx = (v) => v * cellSize;
ctx.globalAlpha = clamp01(shape.strokeOpacity, 1); ctx.globalAlpha = clamp01(shape.strokeOpacity, SHAPE_DEFAULTS.strokeOpacity);
ctx.lineWidth = Math.max(1, toPx(shape.strokeWidth ?? 0.12)); ctx.lineWidth = Math.max(1, toPx(shape.strokeWidth ?? SHAPE_DEFAULTS.strokeWidth));
ctx.lineJoin = 'round'; ctx.lineJoin = 'round';
ctx.lineCap = 'round'; ctx.lineCap = 'round';

View file

@ -13,10 +13,11 @@
</svg> </svg>
<span class="badge text-bg-secondary" data-cell-size-val>25px</span> <span class="badge text-bg-secondary" data-cell-size-val>25px</span>
</button> </button>
<ul class="dropdown-menu"> <ul class="dropdown-menu p-2">
<li> <li>
<div class="small text-secondary mb-1">Cell Size</div>
<input type="range" min="1" max="100" step="1" value="25" data-cell-size <input type="range" min="1" max="100" step="1" value="25" data-cell-size
class="form-range toolbar-range"> class="form-range w-100">
</li> </li>
</ul> </ul>
</div> </div>
@ -30,10 +31,11 @@
</svg> </svg>
<span class="badge text-bg-secondary" data-fill-opacity-val>100%</span> <span class="badge text-bg-secondary" data-fill-opacity-val>100%</span>
</button> </button>
<ul class="dropdown-menu"> <ul class="dropdown-menu p-2">
<li> <li>
<div class="small text-secondary mb-1">Fill Opacity</div>
<input type="range" min="0" max="1" step="0.01" value="1" data-fill-opacity <input type="range" min="0" max="1" step="0.01" value="1" data-fill-opacity
class="form-range toolbar-range"> class="form-range w-100">
</li> </li>
</ul> </ul>
</div> </div>
@ -47,10 +49,11 @@
</svg> </svg>
<span class="badge text-bg-secondary" data-stroke-opacity-val>100%</span> <span class="badge text-bg-secondary" data-stroke-opacity-val>100%</span>
</button> </button>
<ul class="dropdown-menu"> <ul class="dropdown-menu p-2">
<li> <li>
<div class="small text-secondary mb-1">Stroke Opacity</div>
<input type="range" min="0" max="1" step="0.01" value="1" data-stroke-opacity <input type="range" min="0" max="1" step="0.01" value="1" data-stroke-opacity
class="form-range toolbar-range"> class="form-range w-100">
</li> </li>
</ul> </ul>
</div> </div>
@ -65,10 +68,11 @@
</svg> </svg>
<span class="badge text-bg-secondary" data-stroke-width-val>12%</span> <span class="badge text-bg-secondary" data-stroke-width-val>12%</span>
</button> </button>
<ul class="dropdown-menu"> <ul class="dropdown-menu p-2">
<li> <li>
<div class="small text-secondary mb-1">Stroke Width</div>
<input type="range" min="0" max="1" step="0.01" value="0.12" data-stroke-width <input type="range" min="0" max="1" step="0.01" value="0.12" data-stroke-width
class="form-range toolbar-range"> class="form-range w-100">
</li> </li>
</ul> </ul>
</div> </div>