mirror of
https://github.com/xiaoqidun/ofdgo.git
synced 2026-10-30 01:27:26 +08:00
This commit is contained in:
1 parent
215d405c76
commit
71e67be9c1
14 files changed
+596
-36
No files matched your search
@@ -91,6 +91,11 @@
|
||||
</select>
|
||||
<span>对象</span>
|
||||
</div>
|
||||
<div class="editor-group editor-erase">
|
||||
<button id="eraseObjectButton" class="button" type="button" title="整对象擦除" aria-label="整对象擦除" aria-pressed="false">对象</button>
|
||||
<button id="eraseRegionButton" class="button" type="button" title="区域擦除(视觉裁剪)" aria-label="区域擦除" aria-pressed="false">区域</button>
|
||||
<span>擦除</span>
|
||||
</div>
|
||||
<div class="editor-group">
|
||||
<button id="insertTextButton" class="button" type="button" aria-label="添加文字" aria-pressed="false">文字</button>
|
||||
<button id="insertImageButton" class="button" type="button" aria-label="添加图片">图片</button>
|
||||
|
||||
@@ -119,7 +119,8 @@ a:focus-visible {
|
||||
grid-template-columns: repeat(2, max-content);
|
||||
}
|
||||
|
||||
.editor-transform {
|
||||
.editor-transform,
|
||||
.editor-erase {
|
||||
grid-template-columns: max-content;
|
||||
}
|
||||
|
||||
@@ -1400,6 +1401,7 @@ button.outline-link:hover {
|
||||
|
||||
.edit-object:hover,
|
||||
.edit-object.selected,
|
||||
.edit-object.erasing,
|
||||
.edit-selection,
|
||||
.edit-crop {
|
||||
outline: calc(1px / var(--surface-scale)) solid var(--accent);
|
||||
@@ -1419,6 +1421,11 @@ button.outline-link:hover {
|
||||
background: rgba(15, 123, 104, 0.04);
|
||||
}
|
||||
|
||||
.edit-object.erasing {
|
||||
background: var(--accent-soft);
|
||||
outline-style: dashed;
|
||||
}
|
||||
|
||||
.edit-handle {
|
||||
display: none;
|
||||
position: absolute;
|
||||
|
||||
+35
-3
@@ -52,6 +52,7 @@ const state = {
|
||||
searchIndex: -1,
|
||||
navigationScroll: new Map(),
|
||||
fontSyncPending: false,
|
||||
fontCatalogLoading: false,
|
||||
fontRenderPending: false,
|
||||
fontSyncing: false,
|
||||
doc: null,
|
||||
@@ -117,6 +118,8 @@ const el = {
|
||||
drawLineButton: document.querySelector("#drawLineButton"),
|
||||
drawRectangleButton: document.querySelector("#drawRectangleButton"),
|
||||
drawEllipseButton: document.querySelector("#drawEllipseButton"),
|
||||
eraseObjectButton: document.querySelector("#eraseObjectButton"),
|
||||
eraseRegionButton: document.querySelector("#eraseRegionButton"),
|
||||
shapeFill: document.querySelector("#shapeFill"),
|
||||
shapeFillColor: document.querySelector("#shapeFillColor"),
|
||||
shapeStroke: document.querySelector("#shapeStroke"),
|
||||
@@ -246,7 +249,7 @@ const fontManager = new FontManager({
|
||||
onChange: scheduleFontSync,
|
||||
onPermissionChange: updateFontPermissionHint,
|
||||
});
|
||||
const fontPicker = new FontPicker(el.textFont, el.textFontToggle, el.textFontList, changeTextFont);
|
||||
const fontPicker = new FontPicker(el.textFont, el.textFontToggle, el.textFontList, changeTextFont, loadEditorFonts);
|
||||
el.editorTools.addEventListener("scroll", () => fontPicker.position());
|
||||
|
||||
const canvasEditor = new CanvasEditor(el.viewerPanel, {
|
||||
@@ -274,6 +277,13 @@ const canvasEditor = new CanvasEditor(el.viewerPanel, {
|
||||
if (state.fontRenderPending) window.setTimeout(refreshPendingFonts, 0);
|
||||
},
|
||||
onDelete: (item) => changeDocument(item.items ? "ofdgoDeleteObjects" : "ofdgoDeleteObject", item),
|
||||
onErase: (items, box) => {
|
||||
const blocked = items.find(item => !canEditObject(item, box ? "arrange" : "delete"));
|
||||
if (blocked) { setStatus(objectEditReason(blocked) || "对象暂不可擦除"); return false; }
|
||||
const item = { index: items[0].index, id: items.map(item => item.id) };
|
||||
return box ? changeDocument("ofdgoEraseObjects", item, box.x, box.y, box.width, box.height)
|
||||
: changeDocument("ofdgoDeleteObjects", item);
|
||||
},
|
||||
onEdit: editCanvasObject,
|
||||
drawStyle: shapeStyle,
|
||||
onTool: updateDrawingControls,
|
||||
@@ -426,7 +436,8 @@ el.resetCropButton.addEventListener("click", () => {
|
||||
});
|
||||
editorClick(el.undoButton, () => changeDocument("ofdgoUndo"));
|
||||
editorClick(el.redoButton, () => changeDocument("ofdgoRedo"));
|
||||
for (const [button, tool] of [[el.drawLineButton, "line"], [el.drawRectangleButton, "rectangle"], [el.drawEllipseButton, "ellipse"]]) {
|
||||
for (const [button, tool] of [[el.drawLineButton, "line"], [el.drawRectangleButton, "rectangle"], [el.drawEllipseButton, "ellipse"],
|
||||
[el.eraseObjectButton, "erase-object"], [el.eraseRegionButton, "erase-region"]]) {
|
||||
editorClick(button, () => {
|
||||
if (!state.editing || document.body.hasAttribute("aria-busy")) {
|
||||
return;
|
||||
@@ -987,6 +998,23 @@ function editorFonts(item) {
|
||||
return fonts;
|
||||
}
|
||||
|
||||
async function loadEditorFonts() {
|
||||
if (!fontManager.canReadLocal() || fontManager.catalogLoaded || fontManager.permission === "denied" || state.fontCatalogLoading) return;
|
||||
state.fontCatalogLoading = true;
|
||||
try {
|
||||
await fontManager.queryLocal();
|
||||
const open = fontPicker.open;
|
||||
const selected = state.textFonts[Number(fontPicker.value)];
|
||||
const query = el.textFont.value === (selected?.fullName || selected?.name || "") ? "" : el.textFont.value;
|
||||
updateTextFonts(canvasEditor.selected, true);
|
||||
if (open && !el.textFont.disabled) { if (query) el.textFont.value = query; fontPicker.show(query); }
|
||||
} catch (err) {
|
||||
setStatus(err?.name === "NotAllowedError" ? "字体尚未授权" : String(err.message || err));
|
||||
} finally {
|
||||
state.fontCatalogLoading = false;
|
||||
}
|
||||
}
|
||||
|
||||
function updateTextFonts(item, refresh = false) {
|
||||
const id = item?.type === "TextObject" && !item.draft ? item.font : null;
|
||||
if (refresh || state.textFontID !== id) {
|
||||
@@ -1193,6 +1221,10 @@ function updateDrawingControls() {
|
||||
button.disabled = disabled || !pageCan("insert");
|
||||
button.setAttribute("aria-pressed", String(tool === name));
|
||||
}
|
||||
for (const [button, name] of [[el.eraseObjectButton, "erase-object"], [el.eraseRegionButton, "erase-region"]]) {
|
||||
button.disabled = disabled;
|
||||
button.setAttribute("aria-pressed", String(tool === name));
|
||||
}
|
||||
el.selectObjectButton.setAttribute("aria-pressed", String(canvasEditor.enabled && !tool));
|
||||
const styleDisabled = disabled || (canvasEditor.selected?.type === "PathObject"
|
||||
? !canEditObject(canvasEditor.selected, "update") : !pageCan("insert"));
|
||||
@@ -1224,7 +1256,7 @@ async function changeDocument(name, item, ...args) {
|
||||
return true;
|
||||
}
|
||||
setEditorInfo(doc);
|
||||
if (!item || name === "ofdgoDeleteObject" || name === "ofdgoDeleteObjects") {
|
||||
if (!item || name === "ofdgoDeleteObject" || name === "ofdgoDeleteObjects" || name === "ofdgoEraseObjects") {
|
||||
canvasEditor.clear();
|
||||
}
|
||||
if (name === "ofdgoCopyObjects" || name === "ofdgoPasteObjects" || name === "ofdgoInsertShape" || name === "ofdgoInsertText" || name === "ofdgoInsertImage") {
|
||||
|
||||
@@ -358,7 +358,8 @@ export class CanvasEditor {
|
||||
return;
|
||||
}
|
||||
if (this.tool) {
|
||||
this.startShape(event);
|
||||
if (this.tool.startsWith("erase-")) this.startErase(event);
|
||||
else this.startShape(event);
|
||||
return;
|
||||
}
|
||||
if (event.altKey && !event.shiftKey && !event.ctrlKey && !event.metaKey && !event.target.dataset.corner) {
|
||||
@@ -425,6 +426,10 @@ export class CanvasEditor {
|
||||
}
|
||||
event.preventDefault();
|
||||
drag.pointer = event;
|
||||
if (drag.erase) {
|
||||
this.moveErase(event, drag);
|
||||
return;
|
||||
}
|
||||
if (drag.crop) {
|
||||
const from = pagePoint(drag.clientX, drag.clientY, drag.rect, drag.page, drag.rotation);
|
||||
const to = pagePoint(event.clientX, event.clientY, drag.rect, drag.page, drag.rotation);
|
||||
@@ -495,6 +500,14 @@ export class CanvasEditor {
|
||||
return;
|
||||
}
|
||||
this.move(event);
|
||||
if (drag.erase) {
|
||||
const items = drag.erase === "erase-object" ? [...drag.erased] : drag.box
|
||||
? drag.items.filter(item => item.x < drag.box.x + drag.box.width && item.x + item.width > drag.box.x
|
||||
&& item.y < drag.box.y + drag.box.height && item.y + item.height > drag.box.y) : [];
|
||||
this.cancel();
|
||||
if (items.length) this.options.onErase(items, drag.erase === "erase-region" ? drag.box : null);
|
||||
return;
|
||||
}
|
||||
if (drag.crop) {
|
||||
this.drag = null;
|
||||
this.viewer.releasePointerCapture(drag.pointerID);
|
||||
@@ -550,6 +563,7 @@ export class CanvasEditor {
|
||||
const drag = this.drag;
|
||||
this.drag = null;
|
||||
if (drag) {
|
||||
for (const item of drag.erased || []) item.node.classList.remove("erasing");
|
||||
if (drag.crop) {
|
||||
this.crop.box = drag.box;
|
||||
this.paintCrop();
|
||||
@@ -566,6 +580,29 @@ export class CanvasEditor {
|
||||
}
|
||||
}
|
||||
|
||||
startErase(event) {
|
||||
const surface = event.target.closest(".page-surface") || this.nearestSurface(event);
|
||||
const page = this.pages.get(surface);
|
||||
if (!page) return;
|
||||
event.preventDefault();
|
||||
this.viewer.focus({ preventScroll: true });
|
||||
this.drag = { erase: this.tool, page, surface, items: [...surface.querySelectorAll(".edit-object")].map(node => this.nodes.get(node)),
|
||||
erased: new Set(), shape: "rectangle", pointerID: event.pointerId, clientX: event.clientX, clientY: event.clientY,
|
||||
rect: surface.getBoundingClientRect(), rotation: this.options.rotation() };
|
||||
if (this.tool === "erase-region") Object.assign(this.drag, this.createPreview(surface, page, "rectangle",
|
||||
{ fill: true, fillColor: "var(--accent-soft)", stroke: true, strokeColor: "var(--accent)", lineWidth: 0.2 }));
|
||||
this.viewer.setPointerCapture(event.pointerId);
|
||||
this.moveErase(event, this.drag);
|
||||
}
|
||||
|
||||
moveErase(event, drag) {
|
||||
if (drag.erase === "erase-region") { this.moveShape(event, drag); return; }
|
||||
const point = pagePoint(event.clientX, event.clientY, drag.rect, drag.page, drag.rotation);
|
||||
const item = [...drag.items].reverse().find(item => point.x >= item.x && point.x <= item.x + item.width
|
||||
&& point.y >= item.y && point.y <= item.y + item.height);
|
||||
if (item) { drag.erased.add(item); item.node.classList.add("erasing"); }
|
||||
}
|
||||
|
||||
startMarquee(event, additive) {
|
||||
const surface = event.target.closest(".page-surface"), page = this.pages.get(surface);
|
||||
if (!page) {
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
const DATABASE = "ofdgo";
|
||||
|
||||
export class FontPicker {
|
||||
constructor(input, toggle, list, onChange) {
|
||||
Object.assign(this, { input, toggle, list, onChange, fonts: [], value: "", active: -1, open: false });
|
||||
constructor(input, toggle, list, onChange, onOpen) {
|
||||
Object.assign(this, { input, toggle, list, onChange, onOpen, fonts: [], value: "", active: -1, open: false });
|
||||
input.addEventListener("input", () => this.show(input.value));
|
||||
input.addEventListener("focus", () => input.select());
|
||||
input.addEventListener("click", () => { if (!this.open) this.show(); });
|
||||
@@ -73,6 +73,7 @@ export class FontPicker {
|
||||
this.position();
|
||||
const selected = this.matches.findIndex(({ font, index }) => !font.disabled && String(index) === this.value);
|
||||
this.activate(selected < 0 ? this.matches.findIndex(({ font }) => !font.disabled) : selected);
|
||||
this.onOpen?.();
|
||||
}
|
||||
|
||||
position() {
|
||||
|
||||
Reference in new issue
Block a user