Multiple widget support?
This commit is contained in:
parent
5dfc2691e9
commit
ce562d34de
1 changed files with 859 additions and 896 deletions
|
|
@ -1,14 +1,13 @@
|
||||||
{% extends 'base.html' %}
|
{% extends 'base.html' %}
|
||||||
|
|
||||||
{% block style %}
|
{% block style %}
|
||||||
|
.grid-widget .grid-wrap {
|
||||||
#gridWrap {
|
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 80vh;
|
height: 80vh;
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
#grid {
|
.grid-widget [data-grid] {
|
||||||
position: relative;
|
position: relative;
|
||||||
cursor: crosshair;
|
cursor: crosshair;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|
@ -17,27 +16,27 @@
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
#toolBar::-webkit-scrollbar {
|
.grid-widget [data-toolbar]::-webkit-scrollbar {
|
||||||
height: 8px;
|
height: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
#coords {
|
.grid-widget [data-coords] {
|
||||||
bottom: 10px;
|
bottom: 10px;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
left: 10px;
|
left: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
#overlay {
|
.grid-widget [data-canvas] {
|
||||||
z-index: 9999;
|
z-index: 9999;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
inset: 0;
|
inset: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
#toolBar {
|
.grid-widget [data-toolbar] {
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
#dot {
|
.grid-widget [data-dot] {
|
||||||
transform: translate(-50%, -50%);
|
transform: translate(-50%, -50%);
|
||||||
z-index: 10000;
|
z-index: 10000;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
|
|
@ -45,24 +44,25 @@
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block main %}
|
{% block main %}
|
||||||
|
{% macro drawWidget(uid) %}
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<div id="toolBar"
|
<div class="grid-widget" data-grid-widget>
|
||||||
class="btn-toolbar bg-light border border-bottom-0 rounded-bottom-0 border-secondary-subtle rounded p-1 align-items-center flex-nowrap overflow-auto">
|
<div data-toolbar
|
||||||
|
class="btn-toolbar bg-light border border-bottom-0 rounded-bottom-0 border-secondary-subtle rounded p-1 align-items-center flex-nowrap overflow-auto toolbar">
|
||||||
<div class="input-group input-group-sm w-auto flex-nowrap">
|
<div class="input-group input-group-sm w-auto flex-nowrap">
|
||||||
<span class="input-group-text">Cell Size:</span>
|
<span class="input-group-text">Cell Size:</span>
|
||||||
<input type="number" min="1" value="25" name="cellSize" id="cellSize" class="form-control form-control-sm">
|
<input type="number" min="1" value="25" class="form-control form-control-sm" data-cell-size>
|
||||||
|
|
||||||
<span class="input-group-text">Fill Opacity:</span>
|
<span class="input-group-text">Fill Opacity:</span>
|
||||||
<input type="number" min="0" max="1" step=".01" value="1" name="fillOpacity" id="fillOpacity"
|
<input type="number" min="0" max="1" step=".01" value="1" name="fillOpacity" data-fill-opacity
|
||||||
class="form-control form-control-sm">
|
class="form-control form-control-sm">
|
||||||
</div>
|
</div>
|
||||||
<div class="vr mx-1"></div>
|
<div class="vr mx-1"></div>
|
||||||
<input type="color" class="form-control form-control-sm form-control-color" id="color">
|
<input type="color" class="form-control form-control-sm form-control-color" data-color>
|
||||||
<div class="vr mx-1"></div>
|
<div class="vr mx-1"></div>
|
||||||
<div class="btn-group">
|
<div class="btn-group">
|
||||||
<input type="radio" class="btn-check" id="pen" name="tool" checked>
|
<input type="radio" class="btn-check" value="pen" name="tool-{{ uid }}" id="tool-pen-{{ uid }}" data-tool checked>
|
||||||
<label for="pen"
|
<label for="tool-pen-{{ uid }}" class="btn btn-sm btn-light border d-inline-flex align-items-center justify-content-center">
|
||||||
class="btn btn-sm btn-light border d-inline-flex align-items-center justify-content-center">
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-pen"
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-pen"
|
||||||
viewBox="0 0 16 16">
|
viewBox="0 0 16 16">
|
||||||
<path
|
<path
|
||||||
|
|
@ -70,9 +70,8 @@
|
||||||
</svg>
|
</svg>
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
<input type="radio" class="btn-check" id="outline" name="tool">
|
<input type="radio" class="btn-check" value="outline" name="tool-{{ uid }}" id="tool-outline-{{ uid }}" data-tool>
|
||||||
<label for="outline"
|
<label for="tool-outline-{{ uid }}" class="btn btn-sm btn-light border d-inline-flex align-items-center justify-content-center">
|
||||||
class="btn btn-sm btn-light border d-inline-flex align-items-center justify-content-center">
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-square"
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-square"
|
||||||
viewBox="0 0 16 16">
|
viewBox="0 0 16 16">
|
||||||
<path
|
<path
|
||||||
|
|
@ -80,71 +79,63 @@
|
||||||
</svg>
|
</svg>
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
<input type="radio" class="btn-check" id="filled" name="tool">
|
<input type="radio" class="btn-check" value="filled" name="tool-{{ uid }}" id="tool-filled-{{ uid }}" data-tool>
|
||||||
<label for="filled"
|
<label for="tool-filled-{{ uid }}" class="btn btn-sm btn-light border d-inline-flex align-items-center justify-content-center">
|
||||||
class="btn btn-sm btn-light border d-inline-flex align-items-center justify-content-center">
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor"
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor"
|
||||||
class="bi bi-square-fill" viewBox="0 0 16 16">
|
class="bi bi-square-fill" viewBox="0 0 16 16">
|
||||||
<path d="M0 2a2 2 0 0 1 2-2h12a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2z" />
|
<path d="M0 2a2 2 0 0 1 2-2h12a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2z" />
|
||||||
</svg>
|
</svg>
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
<input type="radio" class="btn-check" id="outlineEllipse" name="tool">
|
<input type="radio" class="btn-check" value="outlineEllipse" name="tool-{{ uid }}" id="tool-outline-ellipse-{{ uid }}" data-tool>
|
||||||
<label for="outlineEllipse"
|
<label for="tool-outline-ellipse-{{ uid }}" class="btn btn-sm btn-light border d-inline-flex align-items-center justify-content-center">
|
||||||
class="btn btn-sm btn-light border d-inline-flex align-items-center justify-content-center">
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-circle"
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-circle"
|
||||||
viewBox="0 0 16 16">
|
viewBox="0 0 16 16">
|
||||||
<path d="M8 15A7 7 0 1 1 8 1a7 7 0 0 1 0 14m0 1A8 8 0 1 0 8 0a8 8 0 0 0 0 16" />
|
<path d="M8 15A7 7 0 1 1 8 1a7 7 0 0 1 0 14m0 1A8 8 0 1 0 8 0a8 8 0 0 0 0 16" />
|
||||||
</svg>
|
</svg>
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
<input type="radio" class="btn-check" id="filledEllipse" name="tool">
|
<input type="radio" class="btn-check" value="filledEllipse" name="tool-{{ uid }}" id="tool-filled-ellipse-{{ uid }}" data-tool>
|
||||||
<label for="filledEllipse"
|
<label for="tool-filled-ellipse-{{ uid }}" class="btn btn-sm btn-light border d-inline-flex align-items-center justify-content-center">
|
||||||
class="btn btn-sm btn-light border d-inline-flex align-items-center justify-content-center">
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor"
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor"
|
||||||
class="bi bi-circle-fill" viewBox="0 0 16 16">
|
class="bi bi-circle-fill" viewBox="0 0 16 16">
|
||||||
<circle cx="8" cy="8" r="8" />
|
<circle cx="8" cy="8" r="8" />
|
||||||
</svg>
|
</svg>
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
<input type="radio" class="btn-check" id="line" name="tool">
|
<input type="radio" class="btn-check" value="line" name="tool-{{ uid }}" id="tool-line-{{ uid }}" data-tool>
|
||||||
<label for="line"
|
<label for="tool-line-{{ uid }}" class="btn btn-sm btn-light border d-inline-flex align-items-center justify-content-center">
|
||||||
class="btn btn-sm btn-light border d-inline-flex align-items-center justify-content-center">
|
|
||||||
⎯
|
⎯
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<div class="vr mx-1"></div>
|
<div class="vr mx-1"></div>
|
||||||
<div class="btn-group">
|
<div class="btn-group">
|
||||||
<input type="radio" class="btn-check" name="gridType" id="noGrid" checked>
|
<input type="radio" class="btn-check" name="gridType-{{ uid }}" value="noGrid" id="type-no-grid-{{ uid }}" data-gridtype checked>
|
||||||
<label for="noGrid"
|
<label for="type-no-grid-{{ uid }}" class="btn btn-sm btn-light border d-inline-flex align-items-center justify-content-center">
|
||||||
class="btn btn-sm btn-light border d-inline-flex align-items-center justify-content-center">
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-border"
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-border"
|
||||||
viewBox="0 0 16 16">
|
viewBox="0 0 16 16">
|
||||||
<path
|
<path
|
||||||
d="M0 0h.969v.5H1v.469H.969V1H.5V.969H0zm2.844 1h-.938V0h.938zm1.875 0H3.78V0h.938v1zm1.875 0h-.938V0h.938zm.937 0V.969H7.5V.5h.031V0h.938v.5H8.5v.469h-.031V1zm2.813 0h-.938V0h.938zm1.875 0h-.938V0h.938zm1.875 0h-.938V0h.938zM15.5 1h-.469V.969H15V.5h.031V0H16v.969h-.5zM1 1.906v.938H0v-.938zm6.5.938v-.938h1v.938zm7.5 0v-.938h1v.938zM1 3.78v.938H0V3.78zm6.5.938V3.78h1v.938zm7.5 0V3.78h1v.938zM1 5.656v.938H0v-.938zm6.5.938v-.938h1v.938zm7.5 0v-.938h1v.938zM.969 8.5H.5v-.031H0V7.53h.5V7.5h.469v.031H1v.938H.969zm1.875 0h-.938v-1h.938zm1.875 0H3.78v-1h.938v1zm1.875 0h-.938v-1h.938zm1.875-.031V8.5H7.53v-.031H7.5V7.53h.031V7.5h.938v.031H8.5v.938zm1.875.031h-.938v-1h.938zm1.875 0h-.938v-1h.938zm1.875 0h-.938v-1h.938zm1.406 0h-.469v-.031H15V7.53h.031V7.5h.469v.031h.5v.938h-.5zM0 10.344v-.938h1v.938zm7.5 0v-.938h1v.938zm8.5-.938v.938h-1v-.938zM0 12.22v-.938h1v.938zm7.5 0v-.938h1v.938zm8.5-.938v.938h-1v-.938zM0 14.094v-.938h1v.938zm7.5 0v-.938h1v.938zm8.5-.938v.938h-1v-.938zM.969 16H0v-.969h.5V15h.469v.031H1v.469H.969zm1.875 0h-.938v-1h.938zm1.875 0H3.78v-1h.938v1zm1.875 0h-.938v-1h.938zm.937 0v-.5H7.5v-.469h.031V15h.938v.031H8.5v.469h-.031v.5zm2.813 0h-.938v-1h.938zm1.875 0h-.938v-1h.938zm1.875 0h-.938v-1h.938zm.937 0v-.5H15v-.469h.031V15h.469v.031h.5V16z" />
|
d="M0 0h.969v.5H1v.469H.969V1H.5V.969H0zm2.844 1h-.938V0h.938zm1.875 0H3.78V0h.938v1zm1.875 0h-.938V0h.938zm.937 0V.969H7.5V.5h.031V0h.938v.5H8.5v.469h-.031V1zm2.813 0h-.938V0h.938zm1.875 0h-.938V0h.938zm1.875 0h-.938V0h.938zM15.5 1h-.469V.969H15V.5h.031V0H16v.969h-.5zM1 1.906v.938H0v-.938zm6.5.938v-.938h1v.938zm7.5 0v-.938h1v.938zM1 3.78v.938H0V3.78zm6.5.938V3.78h1v.938zm7.5 0V3.78h1v.938zM1 5.656v.938H0v-.938zm6.5.938v-.938h1v.938zm7.5 0v-.938h1v.938zM.969 8.5H.5v-.031H0V7.53h.5V7.5h.469v.031H1v.938H.969zm1.875 0h-.938v-1h.938zm1.875 0H3.78v-1h.938v1zm1.875 0h-.938v-1h.938zm1.875-.031V8.5H7.53v-.031H7.5V7.53h.031V7.5h.938v.031H8.5v.938zm1.875.031h-.938v-1h.938zm1.875 0h-.938v-1h.938zm1.875 0h-.938v-1h.938zm1.406 0h-.469v-.031H15V7.53h.031V7.5h.469v.031h.5v.938h-.5zM0 10.344v-.938h1v.938zm7.5 0v-.938h1v.938zm8.5-.938v.938h-1v-.938zM0 12.22v-.938h1v.938zm7.5 0v-.938h1v.938zm8.5-.938v.938h-1v-.938zM0 14.094v-.938h1v.938zm7.5 0v-.938h1v.938zm8.5-.938v.938h-1v-.938zM.969 16H0v-.969h.5V15h.469v.031H1v.469H.969zm1.875 0h-.938v-1h.938zm1.875 0H3.78v-1h.938v1zm1.875 0h-.938v-1h.938zm.937 0v-.5H7.5v-.469h.031V15h.938v.031H8.5v.469h-.031v.5zm2.813 0h-.938v-1h.938zm1.875 0h-.938v-1h.938zm1.875 0h-.938v-1h.938zm.937 0v-.5H15v-.469h.031V15h.469v.031h.5V16z" />
|
||||||
</svg>
|
</svg>
|
||||||
</label>
|
</label>
|
||||||
<input type="radio" class="btn-check" name="gridType" id="horizontalGrid">
|
<input type="radio" class="btn-check" name="gridType-{{ uid }}" value="horizontalGrid" id="type-horizontal-{{ uid }}" data-gridtype>
|
||||||
<label for="horizontalGrid"
|
<label for="type-horizontal-{{ uid }}" class="btn btn-sm btn-light border d-inline-flex align-items-center justify-content-center">
|
||||||
class="btn btn-sm btn-light border d-inline-flex align-items-center justify-content-center">
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor"
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor"
|
||||||
class="bi bi-border-center" viewBox="0 0 16 16">
|
class="bi bi-border-center" viewBox="0 0 16 16">
|
||||||
<path
|
<path
|
||||||
d="M.969 0H0v.969h.5V1h.469V.969H1V.5H.969zm.937 1h.938V0h-.938zm1.875 0h.938V0H3.78v1zm1.875 0h.938V0h-.938zM7.531.969V1h.938V.969H8.5V.5h-.031V0H7.53v.5H7.5v.469zM9.406 1h.938V0h-.938zm1.875 0h.938V0h-.938zm1.875 0h.938V0h-.938zm1.875 0h.469V.969h.5V0h-.969v.5H15v.469h.031zM1 2.844v-.938H0v.938zm6.5-.938v.938h1v-.938zm7.5 0v.938h1v-.938zM1 4.719V3.78H0v.938h1zm6.5-.938v.938h1V3.78h-1zm7.5 0v.938h1V3.78h-1zM1 6.594v-.938H0v.938zm6.5-.938v.938h1v-.938zm7.5 0v.938h1v-.938zM0 8.5v-1h16v1zm0 .906v.938h1v-.938zm7.5 0v.938h1v-.938zm8.5.938v-.938h-1v.938zm-16 .937v.938h1v-.938zm7.5 0v.938h1v-.938zm8.5.938v-.938h-1v.938zm-16 .937v.938h1v-.938zm7.5 0v.938h1v-.938zm8.5.938v-.938h-1v.938zM0 16h.969v-.5H1v-.469H.969V15H.5v.031H0zm1.906 0h.938v-1h-.938zm1.875 0h.938v-1H3.78v1zm1.875 0h.938v-1h-.938zm1.875-.5v.5h.938v-.5H8.5v-.469h-.031V15H7.53v.031H7.5v.469zm1.875.5h.938v-1h-.938zm1.875 0h.938v-1h-.938zm1.875 0h.938v-1h-.938zm1.875-.5v.5H16v-.969h-.5V15h-.469v.031H15v.469z" />
|
d="M.969 0H0v.969h.5V1h.469V.969H1V.5H.969zm.937 1h.938V0h-.938zm1.875 0h.938V0H3.78v1zm1.875 0h.938V0h-.938zM7.531.969V1h.938V.969H8.5V.5h-.031V0H7.53v.5H7.5v.469zM9.406 1h.938V0h-.938zm1.875 0h.938V0h-.938zm1.875 0h.938V0h-.938zm1.875 0h.469V.969h.5V0h-.969v.5H15v.469h.031zM1 2.844v-.938H0v.938zm6.5-.938v.938h1v-.938zm7.5 0v.938h1v-.938zM1 4.719V3.78H0v.938h1zm6.5-.938v.938h1V3.78h-1zm7.5 0v.938h1V3.78h-1zM1 6.594v-.938H0v.938zm6.5-.938v.938h1v-.938zm7.5 0v.938h1v-.938zM0 8.5v-1h16v1zm0 .906v.938h1v-.938zm7.5 0v.938h1v-.938zm8.5.938v-.938h-1v.938zm-16 .937v.938h1v-.938zm7.5 0v.938h1v-.938zm8.5.938v-.938h-1v.938zm-16 .937v.938h1v-.938zm7.5 0v.938h1v-.938zm8.5.938v-.938h-1v.938zM0 16h.969v-.5H1v-.469H.969V15H.5v.031H0zm1.906 0h.938v-1h-.938zm1.875 0h.938v-1H3.78v1zm1.875 0h.938v-1h-.938zm1.875-.5v.5h.938v-.5H8.5v-.469h-.031V15H7.53v.031H7.5v.469zm1.875.5h.938v-1h-.938zm1.875 0h.938v-1h-.938zm1.875 0h.938v-1h-.938zm1.875-.5v.5H16v-.969h-.5V15h-.469v.031H15v.469z" />
|
||||||
</svg>
|
</svg>
|
||||||
</label>
|
</label>
|
||||||
<input type="radio" class="btn-check" name="gridType" id="verticalGrid">
|
<input type="radio" class="btn-check" name="gridType-{{ uid }}" value="verticalGrid" id="type-vertical-{{ uid }}" data-gridtype>
|
||||||
<label for="verticalGrid"
|
<label for="type-vertical-{{ uid }}" class="btn btn-sm btn-light border d-inline-flex align-items-center justify-content-center">
|
||||||
class="btn btn-sm btn-light border d-inline-flex align-items-center justify-content-center">
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor"
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor"
|
||||||
class="bi bi-border-middle" viewBox="0 0 16 16">
|
class="bi bi-border-middle" viewBox="0 0 16 16">
|
||||||
<path
|
<path
|
||||||
d="M.969 0H0v.969h.5V1h.469V.969H1V.5H.969zm.937 1h.938V0h-.938zm1.875 0h.938V0H3.78v1zm1.875 0h.938V0h-.938zM8.5 16h-1V0h1zm.906-15h.938V0h-.938zm1.875 0h.938V0h-.938zm1.875 0h.938V0h-.938zm1.875 0h.469V.969h.5V0h-.969v.5H15v.469h.031zM1 2.844v-.938H0v.938zm14-.938v.938h1v-.938zM1 4.719V3.78H0v.938h1zm14-.938v.938h1V3.78h-1zM1 6.594v-.938H0v.938zm14-.938v.938h1v-.938zM.5 8.5h.469v-.031H1V7.53H.969V7.5H.5v.031H0v.938h.5zm1.406 0h.938v-1h-.938zm1.875 0h.938v-1H3.78v1zm1.875 0h.938v-1h-.938zm3.75 0h.938v-1h-.938zm1.875 0h.938v-1h-.938zm1.875 0h.938v-1h-.938zm1.875 0h.469v-.031h.5V7.53h-.5V7.5h-.469v.031H15v.938h.031zM0 9.406v.938h1v-.938zm16 .938v-.938h-1v.938zm-16 .937v.938h1v-.938zm16 .938v-.938h-1v.938zm-16 .937v.938h1v-.938zm16 .938v-.938h-1v.938zM0 16h.969v-.5H1v-.469H.969V15H.5v.031H0zm1.906 0h.938v-1h-.938zm1.875 0h.938v-1H3.78v1zm1.875 0h.938v-1h-.938zm3.75 0h.938v-1h-.938zm1.875 0h.938v-1h-.938zm1.875 0h.938v-1h-.938zm1.875-.5v.5H16v-.969h-.5V15h-.469v.031H15v.469z" />
|
d="M.969 0H0v.969h.5V1h.469V.969H1V.5H.969zm.937 1h.938V0h-.938zm1.875 0h.938V0H3.78v1zm1.875 0h.938V0h-.938zM8.5 16h-1V0h1zm.906-15h.938V0h-.938zm1.875 0h.938V0h-.938zm1.875 0h.938V0h-.938zm1.875 0h.469V.969h.5V0h-.969v.5H15v.469h.031zM1 2.844v-.938H0v.938zm14-.938v.938h1v-.938zM1 4.719V3.78H0v.938h1zm14-.938v.938h1V3.78h-1zM1 6.594v-.938H0v.938zm14-.938v.938h1v-.938zM.5 8.5h.469v-.031H1V7.53H.969V7.5H.5v.031H0v.938h.5zm1.406 0h.938v-1h-.938zm1.875 0h.938v-1H3.78v1zm1.875 0h.938v-1h-.938zm3.75 0h.938v-1h-.938zm1.875 0h.938v-1h-.938zm1.875 0h.938v-1h-.938zm1.875 0h.469v-.031h.5V7.53h-.5V7.5h-.469v.031H15v.938h.031zM0 9.406v.938h1v-.938zm16 .938v-.938h-1v.938zm-16 .937v.938h1v-.938zm16 .938v-.938h-1v.938zm-16 .937v.938h1v-.938zm16 .938v-.938h-1v.938zM0 16h.969v-.5H1v-.469H.969V15H.5v.031H0zm1.906 0h.938v-1h-.938zm1.875 0h.938v-1H3.78v1zm1.875 0h.938v-1h-.938zm3.75 0h.938v-1h-.938zm1.875 0h.938v-1h-.938zm1.875 0h.938v-1h-.938zm1.875-.5v.5H16v-.969h-.5V15h-.469v.031H15v.469z" />
|
||||||
</svg>
|
</svg>
|
||||||
</label>
|
</label>
|
||||||
<input type="radio" class="btn-check" name="gridType" id="fullGrid">
|
<input type="radio" class="btn-check" name="gridType-{{ uid }}" value="fullGrid" id="type-full-{{ uid }}" data-gridtype>
|
||||||
<label for="fullGrid"
|
<label for="type-full-{{ uid }}" class="btn btn-sm btn-light border d-inline-flex align-items-center justify-content-center">
|
||||||
class="btn btn-sm btn-light border d-inline-flex align-items-center justify-content-center">
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor"
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor"
|
||||||
class="bi bi-border-all" viewBox="0 0 16 16">
|
class="bi bi-border-all" viewBox="0 0 16 16">
|
||||||
<path d="M0 0h16v16H0zm1 1v6.5h6.5V1zm7.5 0v6.5H15V1zM15 8.5H8.5V15H15zM7.5 15V8.5H1V15z" />
|
<path d="M0 0h16v16H0zm1 1v6.5h6.5V1zm7.5 0v6.5H15V1zM15 8.5H8.5V15H15zM7.5 15V8.5H1V15z" />
|
||||||
|
|
@ -154,7 +145,7 @@
|
||||||
<div class="vr mx-1"></div>
|
<div class="vr mx-1"></div>
|
||||||
<div class="btn-group">
|
<div class="btn-group">
|
||||||
<button type="button"
|
<button type="button"
|
||||||
class="btn btn-sm btn-light border d-inline-flex align-items-center justify-content-center" id="export">
|
class="btn btn-sm btn-light border d-inline-flex align-items-center justify-content-center" data-export>
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor"
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor"
|
||||||
class="bi bi-download" viewBox="0 0 16 16">
|
class="bi bi-download" viewBox="0 0 16 16">
|
||||||
<path
|
<path
|
||||||
|
|
@ -163,10 +154,10 @@
|
||||||
d="M7.646 11.854a.5.5 0 0 0 .708 0l3-3a.5.5 0 0 0-.708-.708L8.5 10.293V1.5a.5.5 0 0 0-1 0v8.793L5.354 8.146a.5.5 0 1 0-.708.708z" />
|
d="M7.646 11.854a.5.5 0 0 0 .708 0l3-3a.5.5 0 0 0-.708-.708L8.5 10.293V1.5a.5.5 0 0 0-1 0v8.793L5.354 8.146a.5.5 0 1 0-.708.708z" />
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
<input type="file" id="import" accept="application/json" class="d-none">
|
<input type="file" data-import accept="application/json" class="d-none">
|
||||||
<button type="button"
|
<button type="button"
|
||||||
class="btn btn-sm btn-light border d-inline-flex align-items-center justify-content-center"
|
class="btn btn-sm btn-light border d-inline-flex align-items-center justify-content-center"
|
||||||
id="importButton">
|
data-import-button>
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-upload"
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-upload"
|
||||||
viewBox="0 0 16 16">
|
viewBox="0 0 16 16">
|
||||||
<path
|
<path
|
||||||
|
|
@ -176,7 +167,7 @@
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
<button type="button"
|
<button type="button"
|
||||||
class="btn btn-sm btn-danger border d-inline-flex align-items-center justify-content-center" id="clear">
|
class="btn btn-sm btn-danger border d-inline-flex align-items-center justify-content-center" data-clear>
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-x-lg"
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-x-lg"
|
||||||
viewBox="0 0 16 16">
|
viewBox="0 0 16 16">
|
||||||
<path
|
<path
|
||||||
|
|
@ -185,43 +176,68 @@
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div id="gridWrap">
|
<div class="grid-wrap" data-grid-wrap>
|
||||||
<span id="dot" class="position-absolute p-0 m-0 d-none">
|
<span class="position-absolute p-0 m-0 d-none dot" data-dot>
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32" id="dotSVG">
|
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32" data-dot-svg>
|
||||||
<circle cx="16" cy="16" r="4" fill="black" />
|
<circle cx="16" cy="16" r="4" fill="black" />
|
||||||
</svg>
|
</svg>
|
||||||
</span>
|
</span>
|
||||||
<div id="grid" class="position-relative overflow-hidden">
|
<div class="position-relative overflow-hidden grid" data-grid>
|
||||||
<div id="coords"
|
<div class="border border-black position-absolute d-none bg-warning-subtle px-1 py-0 user-select-none coords" data-coords></div>
|
||||||
class="border border-black position-absolute d-none bg-warning-subtle px-1 py-0 user-select-none"></div>
|
<canvas class="position-absolute w-100 h-100" data-canvas></canvas>
|
||||||
<canvas id="overlay" class="position-absolute w-100 h-100"></canvas>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endmacro %}
|
||||||
|
{{ drawWidget('test') }}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block script %}
|
{% block script %}
|
||||||
|
|
||||||
|
document.querySelectorAll('[data-grid-widget]').forEach((root, index) => {
|
||||||
|
initGridWidget(root, { storageKey: `gridDoc:${index}` });
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
let activeGridWidget = null;
|
||||||
|
|
||||||
|
document.addEventListener('keydown', (e) => {
|
||||||
|
if (!activeGridWidget) return;
|
||||||
|
activeGridWidget.handleKeyDown(e);
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
function initGridWidget(root, opts = {}) {
|
||||||
const DEFAULT_DOC = { version: 1, cellSize: 25, shapes: [] };
|
const DEFAULT_DOC = { version: 1, cellSize: 25, shapes: [] };
|
||||||
|
|
||||||
const canvasEl = document.getElementById('overlay');
|
const canvasEl = root.querySelector('[data-canvas]');
|
||||||
const clearEl = document.getElementById('clear');
|
const clearEl = root.querySelector('[data-clear]');
|
||||||
const colorEl = document.getElementById('color');
|
const colorEl = root.querySelector('[data-color]');
|
||||||
const coordsEl = document.getElementById('coords');
|
const coordsEl = root.querySelector('[data-coords]');
|
||||||
const dotEl = document.getElementById('dot');
|
const dotEl = root.querySelector('[data-dot]');
|
||||||
const dotSVGEl = document.getElementById('dotSVG');
|
const dotSVGEl = root.querySelector('[data-dot-svg]');
|
||||||
const exportEl = document.getElementById('export');
|
const exportEl = root.querySelector('[data-export]');
|
||||||
const gridEl = document.getElementById('grid');
|
const gridEl = root.querySelector('[data-grid]');
|
||||||
const importButtonEl = document.getElementById('importButton');
|
const importButtonEl = root.querySelector('[data-import-button]');
|
||||||
const importEl = document.getElementById('import');
|
const importEl = root.querySelector('[data-import]');
|
||||||
const cellSizeEl = document.getElementById('cellSize');
|
const cellSizeEl = root.querySelector('[data-cell-size]');
|
||||||
const gridWrapEl = document.getElementById('gridWrap');
|
const gridWrapEl = root.querySelector('[data-grid-wrap]');
|
||||||
const toolBarEl = document.getElementById('toolBar');
|
const toolBarEl = root.querySelector('[data-toolbar]');
|
||||||
const fillOpacityEl = document.getElementById('fillOpacity');
|
const fillOpacityEl = root.querySelector('[data-fill-opacity]');
|
||||||
|
|
||||||
|
const storageKey = opts.storageKey ?? 'gridDoc';
|
||||||
|
|
||||||
let doc = loadDoc();
|
let doc = loadDoc();
|
||||||
let shapes = sanitizeShapes(Array.isArray(doc.shapes) ? doc.shapes : []);
|
let shapes = sanitizeShapes(Array.isArray(doc.shapes) ? doc.shapes : []);
|
||||||
saveDoc({ ...doc, shapes });
|
saveDoc({ ...doc, shapes });
|
||||||
|
|
||||||
|
const savedTool = localStorage.getItem(`${storageKey}:tool`);
|
||||||
|
if (savedTool) setActiveTool(savedTool);
|
||||||
|
|
||||||
|
const savedType = localStorage.getItem(`${storageKey}:gridType`);
|
||||||
|
if (savedType) setActiveType(savedType);
|
||||||
|
|
||||||
let cellSize = Number(doc.cellSize) || 25;
|
let cellSize = Number(doc.cellSize) || 25;
|
||||||
cellSizeEl.value = cellSize;
|
cellSizeEl.value = cellSize;
|
||||||
let dotSize = Math.floor(Math.max(cellSize * 1.25, 32));
|
let dotSize = Math.floor(Math.max(cellSize * 1.25, 32));
|
||||||
|
|
@ -241,21 +257,47 @@ let lastApplied = { w: 0, h: 0 };
|
||||||
const ro = new ResizeObserver(scheduleSnappedCellSize);
|
const ro = new ResizeObserver(scheduleSnappedCellSize);
|
||||||
ro.observe(gridWrapEl);
|
ro.observe(gridWrapEl);
|
||||||
|
|
||||||
const savedTool = localStorage.getItem('gridTool');
|
|
||||||
if (savedTool) {
|
|
||||||
setActiveTool(savedTool);
|
|
||||||
}
|
|
||||||
|
|
||||||
const savedType = localStorage.getItem('gridType');
|
|
||||||
if (savedType) {
|
|
||||||
setActiveType(savedType);
|
|
||||||
}
|
|
||||||
|
|
||||||
resizeAndSetupCanvas();
|
resizeAndSetupCanvas();
|
||||||
|
|
||||||
setGrid();
|
setGrid();
|
||||||
scheduleSnappedCellSize();
|
scheduleSnappedCellSize();
|
||||||
|
|
||||||
|
const api = {
|
||||||
|
handleKeyDown(e) {
|
||||||
|
const key = e.key.toLowerCase();
|
||||||
|
|
||||||
|
// Only act if the focus isn't in an input inside this widget.
|
||||||
|
// (Otherwise Ctrl+Z in a number field becomes a drawing undo, which is… rude.)
|
||||||
|
const t = e.target;
|
||||||
|
if (t && root.contains(t) && (t.matches('input, textarea, select') || t.isContentEditable)) return;
|
||||||
|
|
||||||
|
if ((e.ctrlKey || e.metaKey) && key === 'z') {
|
||||||
|
e.preventDefault();
|
||||||
|
if (e.shiftKey) redo();
|
||||||
|
else undo();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((e.ctrlKey || e.metaKey) && key === 'y') {
|
||||||
|
e.preventDefault();
|
||||||
|
redo();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (key === 'escape' && currentShape) {
|
||||||
|
e.preventDefault();
|
||||||
|
currentShape = null;
|
||||||
|
redrawAll();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
root.addEventListener('focusin', () => { activeGridWidget = api; });
|
||||||
|
|
||||||
|
root.addEventListener('pointerdown', () => {
|
||||||
|
activeGridWidget = api;
|
||||||
|
}, { capture: true });
|
||||||
|
|
||||||
function renderAllWithPreview(previewShape = null, dashed = true) {
|
function renderAllWithPreview(previewShape = null, dashed = true) {
|
||||||
if (!ctx) return;
|
if (!ctx) return;
|
||||||
clearCanvas();
|
clearCanvas();
|
||||||
|
|
@ -366,13 +408,13 @@ function sanitizeShapes(list) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function loadDoc() {
|
function loadDoc() {
|
||||||
try { return JSON.parse(localStorage.getItem("gridDoc")) || structuredClone(DEFAULT_DOC); }
|
try { return JSON.parse(localStorage.getItem(storageKey)) || structuredClone(DEFAULT_DOC); }
|
||||||
catch { return structuredClone(DEFAULT_DOC); }
|
catch { return structuredClone(DEFAULT_DOC); }
|
||||||
}
|
}
|
||||||
|
|
||||||
function saveDoc(nextDoc = doc) {
|
function saveDoc(nextDoc = doc) {
|
||||||
doc = nextDoc;
|
doc = nextDoc;
|
||||||
try { localStorage.setItem("gridDoc", JSON.stringify(nextDoc)); } catch {}
|
try { localStorage.setItem(storageKey, JSON.stringify(nextDoc)); } catch { }
|
||||||
}
|
}
|
||||||
|
|
||||||
function snapDown(n, step) {
|
function snapDown(n, step) {
|
||||||
|
|
@ -429,29 +471,23 @@ function pxToGrid(v) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function getActiveTool() {
|
function getActiveTool() {
|
||||||
const checked = document.querySelector('input[name="tool"]:checked');
|
const checked = root.querySelector('input[data-tool]:checked');
|
||||||
return checked ? checked.id : 'outline';
|
return checked ? checked.value : 'pen';
|
||||||
}
|
}
|
||||||
|
|
||||||
function setActiveTool(toolId) {
|
function setActiveTool(toolValue) {
|
||||||
const el = document.getElementById(toolId);
|
const el = root.querySelector(`input[data-tool][value="${CSS.escape(toolValue)}"]`);
|
||||||
if (el) {
|
if (el) el.checked = true;
|
||||||
el.checked = true;
|
|
||||||
localStorage.setItem('gridTool', toolId);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function getActiveType() {
|
function getActiveType() {
|
||||||
const checked = document.querySelector('input[name="gridType"]:checked');
|
const checked = root.querySelector('input[data-gridtype]:checked');
|
||||||
return checked ? checked.id : 'noGrid';
|
return checked ? checked.value : 'noGrid';
|
||||||
}
|
}
|
||||||
|
|
||||||
function setActiveType(typeId) {
|
function setActiveType(typeValue) {
|
||||||
const el = document.getElementById(typeId);
|
const el = root.querySelector(`input[data-gridtype][value="${CSS.escape(typeValue)}"]`);
|
||||||
if (el) {
|
if (el) el.checked = true;
|
||||||
el.checked = true;
|
|
||||||
localStorage.setItem('gridType', typeId);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function snapToGrid(x, y) {
|
function snapToGrid(x, y) {
|
||||||
|
|
@ -734,26 +770,26 @@ function onPointerUp(e) {
|
||||||
}
|
}
|
||||||
|
|
||||||
gridEl.addEventListener('pointerup', onPointerUp);
|
gridEl.addEventListener('pointerup', onPointerUp);
|
||||||
window.addEventListener('pointerup', onPointerUp, { capture: true });
|
|
||||||
|
|
||||||
document.querySelectorAll('input[name="tool"]').forEach(input => {
|
root.querySelectorAll('input[data-tool]').forEach((input) => {
|
||||||
input.addEventListener('change', () => {
|
input.addEventListener('change', () => {
|
||||||
if (input.checked) {
|
if (input.checked) {
|
||||||
localStorage.setItem('gridTool', input.id);
|
localStorage.setItem(`${storageKey}:tool`, input.value);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
document.querySelectorAll('input[name="gridType"]').forEach(input => {
|
root.querySelectorAll('input[data-gridtype]').forEach((input) => {
|
||||||
input.addEventListener('change', () => {
|
input.addEventListener('change', () => {
|
||||||
if (input.checked) {
|
if (input.checked) {
|
||||||
localStorage.setItem('gridType', input.id);
|
localStorage.setItem(`${storageKey}:gridType`, input.value);
|
||||||
}
|
}
|
||||||
setGrid();
|
setGrid();
|
||||||
redrawAll();
|
redrawAll();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
cellSizeEl.addEventListener('input', () => applyCellSize(cellSizeEl.value));
|
cellSizeEl.addEventListener('input', () => applyCellSize(cellSizeEl.value));
|
||||||
cellSizeEl.addEventListener('change', () => applyCellSize(cellSizeEl.value));
|
cellSizeEl.addEventListener('change', () => applyCellSize(cellSizeEl.value));
|
||||||
|
|
||||||
|
|
@ -816,31 +852,13 @@ clearEl.addEventListener('click', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
colorEl.addEventListener('input', () => {
|
colorEl.addEventListener('input', () => {
|
||||||
selectedColor = document.getElementById('color').value;
|
selectedColor = colorEl.value || '#000000';
|
||||||
const circle = dotSVGEl.querySelector('circle');
|
const circle = dotSVGEl.querySelector('circle');
|
||||||
if (circle) {
|
if (circle) {
|
||||||
circle.setAttribute('fill', selectedColor);
|
circle.setAttribute('fill', selectedColor);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
document.addEventListener('keydown', (e) => {
|
|
||||||
const key = e.key.toLowerCase();
|
|
||||||
|
|
||||||
if ((e.ctrlKey || e.metaKey) && key === 'z') {
|
|
||||||
e.preventDefault();
|
|
||||||
undo();
|
|
||||||
}
|
|
||||||
|
|
||||||
if ((e.ctrlKey || e.metaKey) && (key === 'y' || (e.shiftKey && key === 'z'))) {
|
|
||||||
e.preventDefault(); redo();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (key === 'escape' && currentShape) {
|
|
||||||
currentShape = null;
|
|
||||||
redrawAll();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
fillOpacityEl?.addEventListener('input', () => {
|
fillOpacityEl?.addEventListener('input', () => {
|
||||||
currentOpacity = clamp01(fillOpacityEl.value, 0);
|
currentOpacity = clamp01(fillOpacityEl.value, 0);
|
||||||
});
|
});
|
||||||
|
|
@ -925,7 +943,7 @@ gridEl.addEventListener('pointerleave', (e) => {
|
||||||
gridEl.addEventListener('pointerdown', (e) => {
|
gridEl.addEventListener('pointerdown', (e) => {
|
||||||
if (e.button !== 0) return;
|
if (e.button !== 0) return;
|
||||||
|
|
||||||
if (e.target.closest('#toolBar')) return;
|
if (e.target.closest('[data-toolbar]')) return;
|
||||||
|
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
gridEl.setPointerCapture(e.pointerId);
|
gridEl.setPointerCapture(e.pointerId);
|
||||||
|
|
@ -977,60 +995,5 @@ gridEl.addEventListener('pointerdown', (e) => {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
window.addEventListener('pointerup', (e) => {
|
|
||||||
if (!currentShape) return;
|
|
||||||
|
|
||||||
if (gridEl.hasPointerCapture?.(e.pointerId)) {
|
|
||||||
gridEl.releasePointerCapture(e.pointerId);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const { x: snapX, y: snapY } = snapToGrid(e.clientX, e.clientY);
|
|
||||||
|
|
||||||
currentShape.x2 = snapX;
|
|
||||||
currentShape.y2 = snapY;
|
|
||||||
|
|
||||||
let finalShape = null;
|
|
||||||
|
|
||||||
if (currentShape.tool === 'pen') {
|
|
||||||
const pts = currentShape.points;
|
|
||||||
|
|
||||||
if (pts.length >= 2) {
|
|
||||||
const simplified = [pts[0]];
|
|
||||||
const minStep = 0.03;
|
|
||||||
for (let i = 1; i < pts.length; i++) {
|
|
||||||
if (dist2(pts[i], simplified[simplified.length - 1]) >= minStep * minStep) {
|
|
||||||
simplified.push(pts[i])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (simplified.length >= 2) {
|
|
||||||
finalShape = { ...currentShape, points: simplified };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (currentShape.tool === 'line') {
|
|
||||||
const line = normalizeLine(currentShape);
|
|
||||||
|
|
||||||
if (line.x1 !== line.x2 || line.y1 !== line.y2) {
|
|
||||||
finalShape = line;
|
|
||||||
}
|
|
||||||
} else if (currentShape.tool === 'filled' || currentShape.tool === 'outline') {
|
|
||||||
const rect = normalizeRect(currentShape);
|
|
||||||
|
|
||||||
if (rect.w > 0 && rect.h > 0) {
|
|
||||||
finalShape = rect;
|
|
||||||
}
|
|
||||||
} else if (currentShape.tool === 'filledEllipse' || currentShape.tool === 'outlineEllipse') {
|
|
||||||
const ellipse = normalizeEllipse(currentShape);
|
|
||||||
if (ellipse.w > 0 && ellipse.h > 0) finalShape = ellipse;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (finalShape) commit([...shapes, finalShape]);
|
|
||||||
|
|
||||||
clearCanvas();
|
|
||||||
shapes.forEach(drawShape);
|
|
||||||
|
|
||||||
currentShape = null;
|
|
||||||
});
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue