mirror of
https://github.com/xiaoqidun/ofdgo.git
synced 2026-10-29 17:27:08 +08:00
This commit is contained in:
1 parent
0508029387
commit
d7cd3d03b5
20 files changed
+1158
-137
No files matched your search
@@ -184,6 +184,31 @@
|
||||
<div class="form-row"><span>组合</span><div class="form-format"><button id="groupObjectsButton" class="button" type="button">组合</button><button id="ungroupObjectButton" class="button" type="button">解组</button></div></div>
|
||||
<div class="form-row"><span>格式</span><div class="form-format"><button id="copyStyleButton" class="button" type="button" aria-label="复制格式">复制</button><button id="pasteStyleButton" class="button" type="button" aria-label="粘贴格式">粘贴</button></div></div>
|
||||
<div class="form-row"><label for="objectOpacity">透明</label><div class="form-unit"><input id="objectOpacity" type="number" min="0" max="100" step="any" aria-label="透明度" autofocus><span>%</span></div></div>
|
||||
<details id="objectGradientFields" hidden>
|
||||
<summary>渐变</summary>
|
||||
<div class="form-pair">
|
||||
<div class="form-row"><label for="gradientTarget">作用</label><select id="gradientTarget"><option value="fill">填充</option><option value="stroke">描边</option></select></div>
|
||||
<div class="form-row"><label for="gradientKind">类型</label><select id="gradientKind"><option value="keep">保持</option><option value="linear">线性</option><option value="radial">径向</option></select></div>
|
||||
</div>
|
||||
<div id="gradientControls" hidden>
|
||||
<div class="form-row"><label for="gradientAngle">角度</label><div class="form-unit"><input id="gradientAngle" type="number" step="any" value="0"><span>°</span></div></div>
|
||||
<div class="gradient-stop-row gradient-stop-head"><span>颜色</span><span>位置%</span><span>透明%</span><span></span></div>
|
||||
<div id="gradientStops"></div>
|
||||
<button id="gradientAdd" class="small-button" type="button" title="添加色标" aria-label="添加色标">+</button>
|
||||
</div>
|
||||
</details>
|
||||
<details id="objectPatternFields" hidden>
|
||||
<summary>图案</summary>
|
||||
<div class="form-pair">
|
||||
<div class="form-row"><label for="patternWidth">宽度</label><div class="form-unit"><input id="patternWidth" type="number" min="0" step="any"><span>mm</span></div></div>
|
||||
<div class="form-row"><label for="patternHeight">高度</label><div class="form-unit"><input id="patternHeight" type="number" min="0" step="any"><span>mm</span></div></div>
|
||||
</div>
|
||||
<div class="form-pair">
|
||||
<div class="form-row"><label for="patternXStep">横距</label><div class="form-unit"><input id="patternXStep" type="number" min="0" step="any"><span>mm</span></div></div>
|
||||
<div class="form-row"><label for="patternYStep">纵距</label><div class="form-unit"><input id="patternYStep" type="number" min="0" step="any"><span>mm</span></div></div>
|
||||
</div>
|
||||
<div class="form-row"><label for="patternCTM">矩阵</label><input id="patternCTM" type="text" spellcheck="false" autocomplete="off"></div>
|
||||
</details>
|
||||
<div id="objectStrokeFields">
|
||||
<div class="form-row"><label for="objectDash">线型</label><select id="objectDash"><option value="mixed" hidden>混合</option><option value="solid">实线</option><option value="dash">虚线</option><option value="dot">点线</option><option value="dashdot">点划</option><option value="custom">自定</option></select></div>
|
||||
<div id="objectDashRow" class="form-row" hidden><label for="objectDashPattern">间隔</label><div class="form-unit"><input id="objectDashPattern" type="text" autocomplete="off" spellcheck="false"><span>mm</span></div></div>
|
||||
@@ -267,6 +292,7 @@
|
||||
<label class="form-row"><span>标题</span><input id="infoTitle" type="text" autofocus></label>
|
||||
<label class="form-row"><span>主题</span><input id="infoSubject" type="text"></label>
|
||||
<label class="form-row"><span>作者</span><input id="infoAuthor" type="text"></label>
|
||||
<details class="info-custom"><summary>扩展</summary><div id="infoCustomRows"></div><button id="infoCustomAdd" class="small-button" type="button" title="添加字段" aria-label="添加字段">+</button></details>
|
||||
<div class="form-actions"><div id="infoStatus" class="form-status" role="status"></div><button id="infoCancel" class="button" type="button">取消</button><button class="button" type="submit">确定</button></div>
|
||||
</form>
|
||||
</dialog>
|
||||
|
||||
@@ -774,6 +774,8 @@ button:not(:disabled):hover {
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
.info-custom-row input,
|
||||
.gradient-stop-row input,
|
||||
.form-row input[type="text"],
|
||||
.form-row input[type="password"],
|
||||
.form-row input[type="number"],
|
||||
@@ -798,6 +800,56 @@ button:not(:disabled):hover {
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
.form-dialog details {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.form-dialog details > summary {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.form-dialog details[open] > summary {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
#infoCustomRows {
|
||||
max-height: 240px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.info-custom-row {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) minmax(0, 2fr) var(--control-height);
|
||||
gap: 6px;
|
||||
margin: 6px 0;
|
||||
}
|
||||
|
||||
#infoCustomRows > :first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.gradient-stop-row {
|
||||
display: grid;
|
||||
grid-template-columns: 44px minmax(0, 1fr) minmax(0, 1fr) var(--control-height);
|
||||
gap: 6px;
|
||||
margin: 6px 0;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.gradient-stop-head {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
#gradientStops {
|
||||
max-height: 180px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
#infoCustomRows :focus-visible,
|
||||
#gradientStops :focus-visible {
|
||||
outline-offset: -2px;
|
||||
}
|
||||
|
||||
.color-input {
|
||||
width: calc(2em + 22px);
|
||||
height: var(--control-height);
|
||||
|
||||
+113
-3
@@ -213,6 +213,8 @@ const el = {
|
||||
infoPanel: document.querySelector("#infoPanel"),
|
||||
infoForm: document.querySelector("#infoForm"),
|
||||
infoTitle: document.querySelector("#infoTitle"),
|
||||
infoCustomRows: document.querySelector("#infoCustomRows"),
|
||||
infoCustomAdd: document.querySelector("#infoCustomAdd"),
|
||||
infoAuthor: document.querySelector("#infoAuthor"),
|
||||
infoSubject: document.querySelector("#infoSubject"),
|
||||
infoCancel: document.querySelector("#infoCancel"),
|
||||
@@ -286,6 +288,19 @@ const el = {
|
||||
groupObjectsButton: document.querySelector("#groupObjectsButton"),
|
||||
ungroupObjectButton: document.querySelector("#ungroupObjectButton"),
|
||||
objectOpacity: document.querySelector("#objectOpacity"),
|
||||
objectGradientFields: document.querySelector("#objectGradientFields"),
|
||||
gradientTarget: document.querySelector("#gradientTarget"),
|
||||
gradientKind: document.querySelector("#gradientKind"),
|
||||
gradientControls: document.querySelector("#gradientControls"),
|
||||
gradientAngle: document.querySelector("#gradientAngle"),
|
||||
gradientStops: document.querySelector("#gradientStops"),
|
||||
gradientAdd: document.querySelector("#gradientAdd"),
|
||||
objectPatternFields: document.querySelector("#objectPatternFields"),
|
||||
patternWidth: document.querySelector("#patternWidth"),
|
||||
patternHeight: document.querySelector("#patternHeight"),
|
||||
patternXStep: document.querySelector("#patternXStep"),
|
||||
patternYStep: document.querySelector("#patternYStep"),
|
||||
patternCTM: document.querySelector("#patternCTM"),
|
||||
objectStrokeFields: document.querySelector("#objectStrokeFields"),
|
||||
objectDash: document.querySelector("#objectDash"),
|
||||
objectDashRow: document.querySelector("#objectDashRow"),
|
||||
@@ -590,14 +605,41 @@ editorClick(el.paragraphButton, () => {
|
||||
el.paragraphCancel.addEventListener("click", () => el.paragraphPanel.close());
|
||||
editorClick(el.infoButton, () => {
|
||||
for (const key of ["Title", "Author", "Subject"]) el[`info${key}`].value = state.doc[key.toLowerCase()] || "";
|
||||
el.infoCustomRows.replaceChildren();
|
||||
for (const [index, field] of (state.doc.customData || []).entries()) addCustomDataRow(field, index);
|
||||
el.infoStatus.textContent = "";
|
||||
el.infoPanel.showModal();
|
||||
});
|
||||
el.infoCancel.addEventListener("click", () => el.infoPanel.close());
|
||||
el.infoCustomAdd.addEventListener("click", () => addCustomDataRow());
|
||||
el.infoForm.addEventListener("submit", async event => {
|
||||
event.preventDefault();
|
||||
if (await changeDocument("ofdgoUpdateInfo", null, el.infoTitle.value, el.infoAuthor.value, el.infoSubject.value)) el.infoPanel.close();
|
||||
const fields = [...el.infoCustomRows.children].map(row => ({Name: row.children[0].value, Value: row.children[1].value, Source: row.sourceIndex}));
|
||||
if (await changeDocument("ofdgoUpdateInfo", null, el.infoTitle.value, el.infoAuthor.value, el.infoSubject.value, JSON.stringify(fields))) el.infoPanel.close();
|
||||
});
|
||||
|
||||
function addCustomDataRow(field = {Name: "", Value: ""}, sourceIndex) {
|
||||
const row = document.createElement("div");
|
||||
row.sourceIndex = sourceIndex;
|
||||
row.className = "info-custom-row";
|
||||
for (const [key, label] of [["Name", "名称"], ["Value", "内容"]]) {
|
||||
const input = document.createElement("input");
|
||||
input.type = "text";
|
||||
input.value = field[key];
|
||||
input.setAttribute("aria-label", label);
|
||||
input.placeholder = label;
|
||||
row.append(input);
|
||||
}
|
||||
const remove = document.createElement("button");
|
||||
remove.type = "button";
|
||||
remove.className = "small-button";
|
||||
remove.textContent = "×";
|
||||
remove.title = "删除字段";
|
||||
remove.setAttribute("aria-label", "删除字段");
|
||||
remove.addEventListener("click", () => row.remove());
|
||||
row.append(remove);
|
||||
el.infoCustomRows.append(row);
|
||||
}
|
||||
el.annotationClose.addEventListener("click", () => el.annotationNote.close());
|
||||
el.annotationNote.addEventListener("close", () => { state.annotationEdit = null; });
|
||||
el.penTool.addEventListener("change", () => {
|
||||
@@ -947,12 +989,22 @@ el.objectBoundsForm.addEventListener("submit", async event => {
|
||||
if (saved) el.objectBoundsPanel.close();
|
||||
});
|
||||
el.objectStyleCancel.addEventListener("click", () => el.objectStylePanel.close());
|
||||
el.gradientKind.addEventListener("change", () => {
|
||||
el.gradientControls.hidden = el.gradientKind.value === "keep";
|
||||
el.gradientAngle.disabled = el.gradientKind.value !== "linear";
|
||||
for (const input of el.gradientStops.querySelectorAll("input")) input.disabled = el.gradientKind.value === "keep";
|
||||
});
|
||||
el.gradientAdd.addEventListener("click", () => addGradientStop(50, "#808080", 0));
|
||||
el.objectDash.addEventListener("change", () => {
|
||||
el.objectDashRow.hidden = el.objectDash.value !== "custom";
|
||||
el.objectDashPattern.required = !el.objectDashRow.hidden;
|
||||
});
|
||||
el.objectStyleForm.addEventListener("submit", async event => {
|
||||
event.preventDefault();
|
||||
if (!el.objectGradientFields.hidden && el.gradientKind.value !== "keep" && el.gradientStops.children.length < 2) {
|
||||
el.objectStyleStatus.textContent = "至少保留两个色标";
|
||||
return;
|
||||
}
|
||||
const style = readObjectStyle();
|
||||
const item = canvasEditor.selected;
|
||||
if (await changeDocument("ofdgoStyleObjects", { ...item, id: canvasEditor.items().map(member => member.id) }, style)) el.objectStylePanel.close();
|
||||
@@ -1390,6 +1442,21 @@ function openObjectStyle() {
|
||||
el.copyStyleButton.disabled = items.length !== 1 || !canCopyStyle(items[0]);
|
||||
el.pasteStyleButton.disabled = !state.styleClipboard || !items.every(item => item.type === state.styleClipboard && canCopyStyle(item));
|
||||
const shared = (key, fallback) => items.every(item => (item[key] ?? fallback) === (items[0][key] ?? fallback)) ? items[0][key] ?? fallback : null;
|
||||
const pattern = items.length === 1 && !items[0].scoped ? items[0].fillPattern : null;
|
||||
el.objectGradientFields.hidden = items.length !== 1 || items[0].scoped || items[0].type !== "PathObject" || !items[0].paintSize || !canEditObject(items[0], "paint");
|
||||
el.gradientKind.value = "keep";
|
||||
el.gradientTarget.value = "fill";
|
||||
el.gradientControls.hidden = true;
|
||||
el.gradientAngle.value = "0";
|
||||
el.gradientAngle.disabled = true;
|
||||
el.gradientStops.replaceChildren();
|
||||
addGradientStop(0, "#000000", 0);
|
||||
addGradientStop(100, "#ffffff", 0);
|
||||
el.objectPatternFields.hidden = !pattern;
|
||||
for (const [input, key] of [[el.patternWidth, "width"], [el.patternHeight, "height"], [el.patternXStep, "xStep"], [el.patternYStep, "yStep"], [el.patternCTM, "ctm"]]) {
|
||||
input.value = pattern?.[key] ?? "";
|
||||
input.disabled = !pattern;
|
||||
}
|
||||
const alpha = items.some(item => item.alphaMixed) ? null : shared("alpha", 255), dash = shared("dashPattern", "");
|
||||
el.objectOpacity.value = alpha === null ? "" : String(Math.round((1 - alpha / 255) * 10000) / 100);
|
||||
el.objectOpacity.placeholder = alpha === null ? "混合" : "";
|
||||
@@ -1401,13 +1468,32 @@ function openObjectStyle() {
|
||||
el.objectDashOffset.value = shared("dashOffset", 0) ?? "";
|
||||
el.objectCap.value = shared("cap", "") || (shared("cap", "") === null ? "mixed" : "Butt");
|
||||
el.objectJoin.value = shared("join", "") || (shared("join", "") === null ? "mixed" : "Miter");
|
||||
state.styleOriginal = Object.fromEntries(["objectOpacity", "objectDash", "objectDashPattern", "objectDashOffset", "objectCap", "objectJoin"].map(key => [key, el[key].value]));
|
||||
state.styleOriginal = Object.fromEntries(["objectOpacity", "objectDash", "objectDashPattern", "objectDashOffset", "objectCap", "objectJoin", "patternWidth", "patternHeight", "patternXStep", "patternYStep", "patternCTM"].map(key => [key, el[key].value]));
|
||||
el.objectStyleStatus.textContent = "";
|
||||
el.objectStylePanel.showModal();
|
||||
}
|
||||
|
||||
function readObjectStyle() {
|
||||
const style = {}, changed = key => el[key].value !== state.styleOriginal[key];
|
||||
if (!el.objectGradientFields.hidden && el.gradientKind.value !== "keep") {
|
||||
const [width, height] = canvasEditor.selected.paintSize;
|
||||
const Segment = [...el.gradientStops.children].map(row => ({Position: Number(row.children[1].value) / 100, Color: {Value: row.children[0].value, Alpha: Math.round(255 * (1 - Number(row.children[2].value) / 100))}})).sort((a, b) => a.Position - b.Position);
|
||||
const angle = Number(el.gradientAngle.value) * Math.PI / 180, x = Math.cos(angle), y = Math.sin(angle);
|
||||
const length = Math.abs(width * x) + Math.abs(height * y);
|
||||
const paint = el.gradientKind.value === "linear"
|
||||
? {AxialShd: {StartPoint: `${width / 2 - x * length / 2} ${height / 2 - y * length / 2}`, EndPoint: `${width / 2 + x * length / 2} ${height / 2 + y * length / 2}`, Extend: "3", Segment}}
|
||||
: {RadialShd: {StartPoint: `${width / 2} ${height / 2}`, EndPoint: `${width / 2} ${height / 2}`, EndRadius: Math.max(width, height) / 2, Extend: "3", Segment}};
|
||||
const target = el.gradientTarget.value;
|
||||
style[`${target}Paint`] = JSON.stringify(paint);
|
||||
style[target] = true;
|
||||
}
|
||||
if (!el.objectPatternFields.hidden) {
|
||||
const pattern = {};
|
||||
for (const [input, key] of [["patternWidth", "Width"], ["patternHeight", "Height"], ["patternXStep", "XStep"], ["patternYStep", "YStep"], ["patternCTM", "CTM"]]) {
|
||||
if (changed(input)) pattern[key] = key === "CTM" ? el[input].value.trim() : Number(el[input].value);
|
||||
}
|
||||
if (Object.keys(pattern).length) style.patternStyle = JSON.stringify(pattern);
|
||||
}
|
||||
if (changed("objectOpacity") && el.objectOpacity.value !== "") style.alpha = Math.round((1 - Number(el.objectOpacity.value) / 100) * 255);
|
||||
if (!el.objectStrokeFields.hidden) {
|
||||
if (changed("objectDash") || changed("objectDashPattern")) {
|
||||
@@ -1421,6 +1507,30 @@ function readObjectStyle() {
|
||||
return style;
|
||||
}
|
||||
|
||||
function addGradientStop(position, color, transparency) {
|
||||
const row = document.createElement("div");
|
||||
row.className = "gradient-stop-row";
|
||||
for (const [type, value, label] of [["color", color, "色标颜色"], ["number", position, "色标位置"], ["number", transparency, "色标透明度"]]) {
|
||||
const input = document.createElement("input");
|
||||
input.type = type;
|
||||
input.value = value;
|
||||
input.disabled = el.gradientKind.value === "keep";
|
||||
input.setAttribute("aria-label", label);
|
||||
input.title = label;
|
||||
if (type === "number") { input.min = "0"; input.max = "100"; input.step = "any"; input.required = true; }
|
||||
row.append(input);
|
||||
}
|
||||
const remove = document.createElement("button");
|
||||
remove.type = "button";
|
||||
remove.className = "small-button";
|
||||
remove.textContent = "×";
|
||||
remove.title = "删除色标";
|
||||
remove.setAttribute("aria-label", "删除色标");
|
||||
remove.addEventListener("click", () => row.remove());
|
||||
row.append(remove);
|
||||
el.gradientStops.append(row);
|
||||
}
|
||||
|
||||
function openOutlinePanel(action) {
|
||||
state.outlineAction = action;
|
||||
let outline = null, items = state.doc.outlines || [];
|
||||
@@ -2439,7 +2549,7 @@ async function changeDocument(name, item, ...args) {
|
||||
}
|
||||
setEditorInfo(doc);
|
||||
if (name === "ofdgoUpdateInfo") {
|
||||
Object.assign(state.doc, { title: doc.title, author: doc.author, subject: doc.subject });
|
||||
Object.assign(state.doc, { title: doc.title, author: doc.author, subject: doc.subject, customData: doc.customData });
|
||||
renderMeta();
|
||||
updateControls();
|
||||
rememberEditorView(before, revision);
|
||||
|
||||
Reference in new issue
Block a user