mirror of
https://github.com/xiaoqidun/ofdgo.git
synced 2026-10-29 17:27:08 +08:00
This commit is contained in:
1 parent
ed11ade4e8
commit
a8346a6006
23 files changed
+2297
-104
No files matched your search
@@ -99,6 +99,8 @@
|
||||
<button id="drawRectangleButton" class="button" type="button" title="矩形" aria-label="矩形" aria-pressed="false">矩形</button>
|
||||
<button id="drawEllipseButton" class="button" type="button" title="椭圆" aria-label="椭圆" aria-pressed="false">椭圆</button>
|
||||
<select id="arrowTool" aria-label="箭头" title="箭头"><option value="" hidden>箭头</option><option value="line">直线</option><option value="arrow">单向</option><option value="double-arrow">双向</option></select>
|
||||
<select id="penTool" aria-label="画笔" title="画笔"><option value="" hidden>画笔</option><option value="pen-round">圆笔</option><option value="pen-pressure">钢笔</option></select>
|
||||
<select id="annotationTool" aria-label="注解工具" title="注解工具"><option value="" hidden>注解</option><option value="note">批注</option><option value="watermark">水印</option><option value="stamp">印章</option><option value="link">链接</option><option value="details">说明</option><option value="replace">替换</option><option value="objects">选取</option></select>
|
||||
<span>插入</span>
|
||||
</div>
|
||||
<div class="editor-group">
|
||||
@@ -264,6 +266,37 @@
|
||||
<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>
|
||||
<dialog id="annotationCreate" class="form-dialog" aria-label="添加注解">
|
||||
<form id="annotationCreateForm">
|
||||
<label class="form-row" data-annotation="watermark"><span>类型</span><select id="annotationContent"><option value="text">文字</option><option value="image">图片</option></select></label>
|
||||
<label class="form-row" data-annotation="replace"><span>原文</span><select id="annotationOld"></select></label>
|
||||
<label class="form-row" data-annotation="replace"><span>字体</span><select id="annotationFont"><option value="keep">原字体</option><option value="current">当前字体</option></select></label>
|
||||
<label class="form-row" id="annotationTextRow"><span>内容</span><textarea id="annotationText" rows="2" required></textarea></label>
|
||||
<label class="form-row" id="annotationImageRow" hidden><span>图片</span><input id="annotationImage" type="file" accept="image/png,image/jpeg,image/bmp,image/tiff,image/gif,image/webp"></label>
|
||||
<label class="form-row" data-annotation="note watermark stamp link"><span>作者</span><input id="annotationAuthor" type="text"></label>
|
||||
<label class="form-row" data-annotation="link"><span>目标</span><select id="annotationLinkKind"><option value="uri">网址</option><option value="page">页面</option><option value="keep" hidden>原链接</option></select></label>
|
||||
<label class="form-row" data-annotation="link" id="annotationAddressRow"><span>网址</span><input id="annotationAddress" type="text"></label>
|
||||
<label class="form-row" data-annotation="link" id="annotationTargetRow"><span>页码</span><input id="annotationTarget" type="number" min="1" value="1"></label>
|
||||
<div class="form-pair" data-annotation="note watermark stamp link">
|
||||
<label class="form-row"><span>横向</span><span class="form-unit"><input id="annotationX" type="number" step="any" value="10" required><span>mm</span></span></label>
|
||||
<label class="form-row"><span>纵向</span><span class="form-unit"><input id="annotationY" type="number" step="any" value="10" required><span>mm</span></span></label>
|
||||
</div>
|
||||
<div class="form-pair" data-annotation="watermark stamp link">
|
||||
<label class="form-row"><span>宽度</span><span class="form-unit"><input id="annotationWidth" type="number" min="1" step="any" value="60" required><span>mm</span></span></label>
|
||||
<label class="form-row"><span>高度</span><span class="form-unit"><input id="annotationHeight" type="number" min="1" step="any" value="12" required><span>mm</span></span></label>
|
||||
</div>
|
||||
<div class="form-pair" data-annotation="watermark stamp">
|
||||
<label class="form-row"><span>角度</span><span class="form-unit"><input id="annotationAngle" type="number" min="-180" max="180" step="any" value="-30" required><span>°</span></span></label>
|
||||
<label class="form-row"><span>透明</span><span class="form-unit"><input id="annotationOpacity" type="number" min="0" max="100" step="any" value="80" aria-label="透明度" required><span>%</span></span></label>
|
||||
</div>
|
||||
<label class="form-row" data-annotation="watermark"><span>排列</span><select id="annotationTile"><option value="single">单个</option><option value="tile">平铺</option></select></label>
|
||||
<label class="form-row" data-annotation="note watermark stamp link"><span>页面</span><input id="annotationPages" type="text" required></label>
|
||||
<div class="form-actions"><div id="annotationCreateStatus" class="form-status" role="status"></div><button id="annotationCreateCancel" class="button" type="button">取消</button><button class="button" type="submit">确定</button></div>
|
||||
</form>
|
||||
</dialog>
|
||||
<dialog id="objectPicker" class="form-dialog" aria-label="选取对象">
|
||||
<form method="dialog"><div id="objectPickerList" class="object-picker-list"></div><div class="form-actions"><button class="button" type="submit">关闭</button></div></form>
|
||||
</dialog>
|
||||
<dialog id="annotationNote" class="form-dialog" aria-label="注解">
|
||||
<form id="annotationForm" method="dialog">
|
||||
<div id="annotationDetail" class="annotation-detail"></div>
|
||||
|
||||
+21
-7
@@ -134,6 +134,24 @@ a:focus-visible {
|
||||
grid-template-columns: repeat(4, max-content);
|
||||
}
|
||||
|
||||
.editor-group:has(#penTool) {
|
||||
grid-template-columns: repeat(4, max-content);
|
||||
}
|
||||
|
||||
.object-picker-list {
|
||||
display: grid;
|
||||
gap: 4px;
|
||||
max-height: 55vh;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.object-picker-list .button {
|
||||
text-align: left;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.editor-field {
|
||||
display: grid;
|
||||
align-items: center;
|
||||
@@ -1516,10 +1534,6 @@ button.outline-link:hover {
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.edit-object.edit-background:not(.selected):not(.erasing) {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.edit-object.edit-contour,
|
||||
.edit-object.edit-contour.selected,
|
||||
.edit-object.edit-contour.erasing {
|
||||
@@ -2310,16 +2324,16 @@ button.outline-link:hover {
|
||||
|
||||
@media (min-width: 901px) and (max-width: 1600px) and (pointer: fine) {
|
||||
.editor-group {
|
||||
padding-inline: 5px;
|
||||
padding-inline: 3px;
|
||||
column-gap: 3px;
|
||||
}
|
||||
|
||||
.editor-toolbar .button {
|
||||
padding-inline: 7px;
|
||||
padding-inline: 6px;
|
||||
}
|
||||
|
||||
.editor-group:has(#textFont) {
|
||||
grid-template-columns: 144px 32px;
|
||||
grid-template-columns: 136px 32px;
|
||||
}
|
||||
|
||||
.editor-paragraph {
|
||||
|
||||
+203
-38
@@ -28,6 +28,7 @@ let textMeasure = null;
|
||||
const state = {
|
||||
composite: null,
|
||||
annotationEdit: null,
|
||||
annotationCreate: null,
|
||||
ready: false,
|
||||
wasmExited: false,
|
||||
wasmSeq: 0,
|
||||
@@ -335,6 +336,12 @@ const el = {
|
||||
signatureSummary: document.querySelector("#signatureSummary"),
|
||||
signatureList: document.querySelector("#signatureList"),
|
||||
annotationNote: document.querySelector("#annotationNote"),
|
||||
annotationTool: document.querySelector("#annotationTool"),
|
||||
penTool: document.querySelector("#penTool"),
|
||||
annotationCreate: document.querySelector("#annotationCreate"),
|
||||
annotationCreateStatus: document.querySelector("#annotationCreateStatus"),
|
||||
objectPicker: document.querySelector("#objectPicker"),
|
||||
objectPickerList: document.querySelector("#objectPickerList"),
|
||||
annotationForm: document.querySelector("#annotationForm"),
|
||||
annotationFields: document.querySelector("#annotationFields"),
|
||||
annotationRemark: document.querySelector("#annotationRemark"),
|
||||
@@ -356,6 +363,7 @@ const fontManager = new FontManager({
|
||||
onPermissionChange: updateFontPermissionHint,
|
||||
});
|
||||
const fontPicker = new FontPicker(el.textFont, el.textFontToggle, el.textFontList, changeTextFont, loadEditorFonts);
|
||||
const annotationFields = Object.fromEntries(["CreateForm", "CreateCancel", "Content", "Old", "Font", "Text", "TextRow", "Image", "ImageRow", "Author", "LinkKind", "Address", "AddressRow", "Target", "TargetRow", "X", "Y", "Width", "Height", "Angle", "Opacity", "Tile", "Pages"].map(key => [key, document.querySelector(`#annotation${key}`)]));
|
||||
el.editorTools.addEventListener("scroll", () => fontPicker.position());
|
||||
|
||||
const canvasEditor = new CanvasEditor(el.viewerPanel, {
|
||||
@@ -371,9 +379,7 @@ const canvasEditor = new CanvasEditor(el.viewerPanel, {
|
||||
setStatus(reason || pageStatus(state.pageIndex, state.doc.pageCount));
|
||||
}
|
||||
},
|
||||
onTransform: (item, change) => (item.items || [item]).every(object => object.type === "Annotation")
|
||||
? changeAnnotations(item, "move", change.x, change.y)
|
||||
: changeDocument(item.items ? "ofdgoTransformObjects" : "ofdgoTransformObject", item,
|
||||
onTransform: (item, change) => changeDocument(item.items ? "ofdgoTransformObjects" : "ofdgoTransformObject", item,
|
||||
change.x + item.x * (1 - change.scale), change.y + item.y * (1 - change.scale), change.scale),
|
||||
onNudgeChange: () => {
|
||||
updatePendingChanges();
|
||||
@@ -389,13 +395,8 @@ const canvasEditor = new CanvasEditor(el.viewerPanel, {
|
||||
syncSelection();
|
||||
if (state.fontRenderPending) window.setTimeout(refreshPendingFonts, 0);
|
||||
},
|
||||
onDelete: (item) => (item.items || [item]).every(object => object.type === "Annotation")
|
||||
? changeAnnotations(item, "delete") : changeDocument(item.items ? "ofdgoDeleteObjects" : "ofdgoDeleteObject", item),
|
||||
onDelete: (item) => changeDocument(item.items ? "ofdgoDeleteObjects" : "ofdgoDeleteObject", item),
|
||||
onErase: (items, box, points) => {
|
||||
if (items.some(item => item.type === "Annotation")) {
|
||||
if (box || !items.every(item => item.type === "Annotation")) { setStatus(box ? "注解暂不支持局部擦除" : "注解与正文需分别操作"); return false; }
|
||||
return changeAnnotations({ items, index: items[0].index, id: items.map(item => item.id) }, "delete");
|
||||
}
|
||||
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), scoped: items.every(item => item.scoped) };
|
||||
@@ -404,6 +405,7 @@ const canvasEditor = new CanvasEditor(el.viewerPanel, {
|
||||
: changeDocument("ofdgoDeleteObjects", item);
|
||||
},
|
||||
onEdit: editCanvasObject,
|
||||
onPick: openObjectPicker,
|
||||
onExitScope: () => {
|
||||
if (!state.composite) return false;
|
||||
exitCompositeScope();
|
||||
@@ -414,6 +416,8 @@ const canvasEditor = new CanvasEditor(el.viewerPanel, {
|
||||
onDraw: (index, shape, box, style) => changeDocument("ofdgoInsertShape", null, index, shape,
|
||||
box.x, box.y, box.width, box.height, style.fill, style.fillColor, style.stroke, style.strokeColor, style.lineWidth),
|
||||
onDrawText: beginCanvasText,
|
||||
onDrawAnnotation: (index, kind, box) => openAnnotationCreate(kind, index, box),
|
||||
onDrawInk: (index, points, style, pressure) => changeDocument("ofdgoInsertInk", null, index, JSON.stringify(points), style.lineWidth, pressure, style.strokeColor),
|
||||
onCommitText: (item, value, fontData, color = null) => item.draft
|
||||
? changeDocument("ofdgoInsertText", null, item.index, value, fontData || item.fontData, item.x, item.y, item.size, item.color, item.width, item.wrap, item.align, item.paragraphHeight, item.letterSpacing, ...textIndents(item))
|
||||
: changeDocument("ofdgoUpdateText", item, value, fontData || null, item.size, color),
|
||||
@@ -497,6 +501,86 @@ el.infoForm.addEventListener("submit", async event => {
|
||||
});
|
||||
el.annotationClose.addEventListener("click", () => el.annotationNote.close());
|
||||
el.annotationNote.addEventListener("close", () => { state.annotationEdit = null; });
|
||||
el.penTool.addEventListener("change", () => {
|
||||
const tool = el.penTool.value;
|
||||
el.penTool.value = "";
|
||||
canvasEditor.select(null);
|
||||
toggleDrawingTool(tool);
|
||||
});
|
||||
el.annotationTool.addEventListener("change", async () => {
|
||||
const kind = el.annotationTool.value;
|
||||
el.annotationTool.value = "";
|
||||
if (kind === "objects") { openObjectPicker(); return; }
|
||||
let selected = canvasEditor.selected;
|
||||
if (state.composite?.key.startsWith("annotation:")) {
|
||||
const id = state.composite.key.split("/")[0];
|
||||
selected = { ...state.pageCache.get(state.composite.index)?.objects.find(item => item.id === id), index: state.composite.index, id, type: "Annotation" };
|
||||
}
|
||||
if (kind === "details" || kind === "replace") {
|
||||
if (selected?.type !== "Annotation") { setStatus("请先选择注解"); return; }
|
||||
try {
|
||||
const info = await callWASM("ofdgoReadAnnotation", selected.index, selected.id);
|
||||
if (kind === "details") editAnnotationDetails({ ...selected, ...info });
|
||||
else openAnnotationCreate(kind, selected.index, null, selected, info);
|
||||
} catch (error) { showError(error, false); }
|
||||
return;
|
||||
}
|
||||
if (kind === "link" && selected?.annotationType === "Link") {
|
||||
try {
|
||||
const info = await callWASM("ofdgoReadAnnotation", selected.index, selected.id);
|
||||
openAnnotationCreate(kind, selected.index, selected, selected, info);
|
||||
} catch (error) { showError(error, false); }
|
||||
} else {
|
||||
resetCompositeScope();
|
||||
updateControls();
|
||||
if (kind === "watermark") openAnnotationCreate(kind, state.pageIndex);
|
||||
else {
|
||||
canvasEditor.select(null);
|
||||
toggleDrawingTool(`annotation:${kind}`);
|
||||
}
|
||||
}
|
||||
});
|
||||
annotationFields.CreateCancel.addEventListener("click", () => el.annotationCreate.close());
|
||||
el.annotationCreate.addEventListener("close", () => { state.annotationCreate = null; });
|
||||
annotationFields.Content.addEventListener("change", updateAnnotationFields);
|
||||
annotationFields.Height.addEventListener("input", () => { state.annotationCreate.height = annotationFields.Height.value; });
|
||||
annotationFields.LinkKind.addEventListener("change", updateAnnotationFields);
|
||||
annotationFields.Old.addEventListener("change", () => { annotationFields.Text.value = annotationFields.Old.value; });
|
||||
annotationFields.Tile.addEventListener("change", () => {
|
||||
if (annotationFields.Tile.value === "tile") {
|
||||
annotationFields.X.value = 10;
|
||||
annotationFields.Y.value = 10;
|
||||
}
|
||||
});
|
||||
annotationFields.CreateForm.addEventListener("submit", async event => {
|
||||
event.preventDefault();
|
||||
const entry = state.annotationCreate;
|
||||
if (!entry || !annotationFields.CreateForm.reportValidity() || document.body.hasAttribute("aria-busy")) return;
|
||||
const openSeq = state.openSeq;
|
||||
const value = key => annotationFields[key].value;
|
||||
const image = entry.kind === "stamp" || entry.kind === "watermark" && value("Content") === "image";
|
||||
let font = null, data = null;
|
||||
try {
|
||||
if (image) data = new Uint8Array(await annotationFields.Image.files[0].arrayBuffer());
|
||||
if (entry.kind === "watermark" && !image || entry.kind === "replace" && value("Font") === "current") {
|
||||
const choice = state.textDefaults.fontChoice || state.textFonts[Number(fontPicker.value)];
|
||||
if (!choice || choice.disabled) throw new Error("请先选择可用字体");
|
||||
font = await readTextFont(choice);
|
||||
}
|
||||
if (openSeq !== state.openSeq || state.annotationCreate !== entry) return;
|
||||
const options = { kind: entry.kind, id: entry.item?.id.replace("annotation:", "") || "", text: value("Text"), old: value("Old"),
|
||||
creator: value("Author"), pages: value("Pages"), uri: value("Address"), target: value("LinkKind") === "page" ? Number(value("Target")) : 0,
|
||||
x: Number(value("X")), y: Number(value("Y")), width: entry.kind === "note" ? 5 : Number(value("Width")), height: entry.kind === "note" ? 5 : Number(value("Height")),
|
||||
angle: Number(value("Angle")), alpha: Math.round((100 - Number(value("Opacity"))) * 255 / 100), tile: value("Tile") === "tile",
|
||||
size: state.textDefaults.size, color: state.textDefaults.color };
|
||||
if (entry.kind === "link" && entry.item) {
|
||||
options.base = entry.info?.linkBase || "";
|
||||
options.keep = value("LinkKind") === "keep" || value("LinkKind") === entry.info?.linkKind
|
||||
&& (value("LinkKind") === "uri" ? value("Address") === entry.info.linkURI : Number(value("Target")) === entry.info.linkPage);
|
||||
}
|
||||
if (await changeDocument("ofdgoWriteAnnotation", null, entry.index, JSON.stringify(options), font, data)) el.annotationCreate.close();
|
||||
} catch (error) { el.annotationCreateStatus.textContent = error.message; }
|
||||
});
|
||||
el.annotationForm.addEventListener("submit", async event => {
|
||||
if (!state.annotationEdit) return;
|
||||
event.preventDefault();
|
||||
@@ -1021,7 +1105,7 @@ function handleKeyDown(event) {
|
||||
|
||||
function formDialogOpen() {
|
||||
return el.exportPanel.open || el.createPanel.open || el.insertPanel.open || el.pagePanel.open || el.paragraphPanel.open || el.importPanel.open || el.infoPanel.open
|
||||
|| el.batchPagesPanel.open || el.objectStylePanel.open || el.outlinePanel.open || el.objectBoundsPanel.open || el.annotationNote.open;
|
||||
|| el.batchPagesPanel.open || el.objectStylePanel.open || el.outlinePanel.open || el.objectBoundsPanel.open || el.annotationNote.open || el.annotationCreate.open || el.objectPicker.open;
|
||||
}
|
||||
|
||||
function setObjectDimension(input, value) {
|
||||
@@ -1053,6 +1137,7 @@ function openObjectBounds() {
|
||||
}
|
||||
|
||||
function canCopyStyle(item) {
|
||||
if (item?.type === "Annotation") return false;
|
||||
if (!item?.scoped) return canEditObject(item, "update");
|
||||
if (item.type === "TextObject") return canEditObject(item, "replaceFont") && canEditObject(item, "paint");
|
||||
if (item.type === "PathObject") return canEditObject(item, "transform") && canEditObject(item, "paint");
|
||||
@@ -1068,7 +1153,7 @@ 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 alpha = shared("alpha", 255), dash = shared("dashPattern", "");
|
||||
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 ? "混合" : "";
|
||||
el.objectStrokeFields.hidden = !items.every(item => item.type === "PathObject" && (!item.scoped || canEditObject(item, "paint")));
|
||||
@@ -1393,21 +1478,96 @@ async function createDocument(event) {
|
||||
}
|
||||
}
|
||||
|
||||
function editAnnotationDetails(item) {
|
||||
state.annotationEdit = item;
|
||||
el.annotationRemark.value = item.remark || "";
|
||||
el.annotationCreator.value = item.creator || "";
|
||||
el.annotationFields.hidden = el.annotationSubmit.hidden = false;
|
||||
el.annotationDetail.hidden = true;
|
||||
el.annotationStatus.textContent = "";
|
||||
el.annotationClose.textContent = "取消";
|
||||
el.annotationNote.showModal();
|
||||
el.annotationRemark.focus();
|
||||
}
|
||||
|
||||
function openObjectPicker(items = canvasEditor.pageItems(state.pageIndex)) {
|
||||
el.objectPickerList.replaceChildren();
|
||||
const labels = { TextObject: "文字", PathObject: "图形", ImageObject: "图片", CompositeObject: "组合", CompositeGraphicUnit: "组合", Annotation: "注解" };
|
||||
const kinds = { Path: "批注", Stamp: "印章", Watermark: "水印", Link: "链接", Highlight: "高亮" };
|
||||
for (const item of [...items].reverse()) {
|
||||
const button = document.createElement("button");
|
||||
button.className = "button";
|
||||
button.type = "button";
|
||||
button.textContent = `${kinds[item.annotationType] || labels[item.type] || "对象"} · ${item.text || item.remark || item.id.replace("annotation:", "")}`;
|
||||
button.title = button.textContent;
|
||||
button.addEventListener("click", () => {
|
||||
el.objectPicker.close();
|
||||
canvasEditor.select(item);
|
||||
canvasEditor.focus();
|
||||
});
|
||||
el.objectPickerList.append(button);
|
||||
}
|
||||
if (!items.length) { setStatus("页面暂无对象"); return; }
|
||||
el.objectPicker.showModal();
|
||||
}
|
||||
|
||||
function openAnnotationCreate(kind, index, box = null, item = null, info = null) {
|
||||
const page = state.doc.pages[index];
|
||||
const width = Math.min(60, page.width), height = 12;
|
||||
state.annotationCreate = { kind, index, item, info, height: box?.height || height };
|
||||
annotationFields.CreateForm.reset();
|
||||
annotationFields.Text.value = kind === "watermark" ? "水印" : "";
|
||||
annotationFields.Pages.value = String(index + 1);
|
||||
annotationFields.Target.max = state.doc.pageCount;
|
||||
annotationFields.Target.value = String(index + 1);
|
||||
annotationFields.LinkKind.querySelector('[value="keep"]').hidden = !item;
|
||||
if (kind === "link" && info) {
|
||||
annotationFields.LinkKind.value = info.linkKind;
|
||||
annotationFields.Address.value = info.linkURI || "";
|
||||
if (info.linkPage) annotationFields.Target.value = String(info.linkPage);
|
||||
}
|
||||
annotationFields.X.value = box?.x ?? (page.width - width) / 2;
|
||||
annotationFields.Y.value = box?.y ?? (page.height - height) / 2;
|
||||
annotationFields.Width.value = box?.width || width;
|
||||
annotationFields.Angle.value = kind === "watermark" ? -30 : 0;
|
||||
annotationFields.Opacity.value = kind === "watermark" ? 80 : 0;
|
||||
annotationFields.Old.replaceChildren(...(info?.texts || []).map(text => new Option(text, text)));
|
||||
if (kind === "replace") annotationFields.Text.value = info?.texts[0] || "";
|
||||
el.annotationCreateStatus.textContent = "";
|
||||
el.annotationCreate.setAttribute("aria-label", {note:"添加批注",watermark:"添加水印",stamp:"添加印章",link: item ? "修改链接" : "添加链接",replace:"替换文字"}[kind]);
|
||||
updateAnnotationFields();
|
||||
el.annotationCreate.showModal();
|
||||
const focus = [annotationFields.Text, annotationFields.Image, annotationFields.Address].find(input => !input.disabled);
|
||||
focus?.focus();
|
||||
}
|
||||
|
||||
function updateAnnotationFields() {
|
||||
const { kind, item } = state.annotationCreate;
|
||||
for (const key of ["X", "Y", "Width", "Height", "Pages", "Author"]) annotationFields[key].closest(".form-row").hidden = false;
|
||||
for (const row of el.annotationCreate.querySelectorAll("[data-annotation]")) row.hidden = !row.dataset.annotation.split(" ").includes(kind);
|
||||
const image = kind === "stamp" || kind === "watermark" && annotationFields.Content.value === "image";
|
||||
annotationFields.ImageRow.hidden = !image;
|
||||
annotationFields.Image.required = image;
|
||||
annotationFields.TextRow.hidden = !["note", "replace", "watermark"].includes(kind) || image;
|
||||
annotationFields.AddressRow.hidden = kind !== "link" || annotationFields.LinkKind.value !== "uri";
|
||||
annotationFields.TargetRow.hidden = kind !== "link" || annotationFields.LinkKind.value !== "page";
|
||||
annotationFields.Height.value = image ? "" : state.annotationCreate.height;
|
||||
annotationFields.Height.placeholder = image ? "等比" : "";
|
||||
if (kind === "link" && item) {
|
||||
for (const key of ["X", "Y", "Width", "Height", "Pages", "Author"]) annotationFields[key].closest(".form-row").hidden = true;
|
||||
}
|
||||
for (const input of annotationFields.CreateForm.querySelectorAll("input,select,textarea")) {
|
||||
input.disabled = Boolean(input.closest("[hidden]")) || image && input === annotationFields.Height;
|
||||
}
|
||||
}
|
||||
|
||||
async function editCanvasObject(item) {
|
||||
if (!state.editing || document.body.hasAttribute("aria-busy") || canvasEditor.input || item.items) return;
|
||||
if (canEditObject(item, "enter")) return enterCompositeScope(item.index, item.id);
|
||||
if (item.type === "Annotation") {
|
||||
state.annotationEdit = item;
|
||||
el.annotationRemark.value = item.remark || "";
|
||||
el.annotationCreator.value = item.creator || "";
|
||||
el.annotationFields.hidden = el.annotationSubmit.hidden = false;
|
||||
el.annotationDetail.hidden = true;
|
||||
el.annotationStatus.textContent = "";
|
||||
el.annotationClose.textContent = "取消";
|
||||
el.annotationNote.showModal();
|
||||
el.annotationRemark.focus();
|
||||
editAnnotationDetails(item);
|
||||
return;
|
||||
}
|
||||
if (canEditObject(item, "enter")) return enterCompositeScope(item.index, item.id);
|
||||
if (item.type === "TextObject") {
|
||||
if (!confirmTextReflow(item)) return;
|
||||
} else if (!canEditObject(item, item.type === "ImageObject" ? "replaceImage" : "update")) return;
|
||||
@@ -1440,13 +1600,7 @@ async function editCanvasObject(item) {
|
||||
|
||||
function mountEditorObjects(index, page, surface) {
|
||||
const scope = state.composite?.index === index ? state.composite : null;
|
||||
const annotations = state.renderAnnotations ? (page.annotations || []).filter(annotation => annotation.visible).map(annotation => ({
|
||||
...annotation, id: `annotation:${annotation.id}`, type: "Annotation", width: annotation.width || 6, height: annotation.height || 6,
|
||||
background: annotation.type === "Watermark" || annotation.x <= 0.1 && annotation.y <= 0.1
|
||||
&& annotation.x + annotation.width >= page.width - 0.1 && annotation.y + annotation.height >= page.height - 0.1,
|
||||
capabilities: { update: true, move: true, delete: true },
|
||||
})) : [];
|
||||
canvasEditor.mount(index, { ...page, objects: scope ? scope.objects : [...page.objects, ...annotations] }, surface);
|
||||
canvasEditor.mount(index, { ...page, objects: scope ? scope.objects : page.objects.filter(item => state.renderAnnotations || item.type !== "Annotation") }, surface);
|
||||
}
|
||||
|
||||
function changeAnnotations(item, action, ...args) {
|
||||
@@ -1904,7 +2058,7 @@ function updateDrawingControls() {
|
||||
const tool = canvasEditor.tool;
|
||||
const path = selectedPath(canvasEditor.selected);
|
||||
el.insertTextButton.setAttribute("aria-pressed", String(tool === "text"));
|
||||
const line = lineShape(tool) || !tool && lineShape(path?.shape);
|
||||
const line = lineShape(tool) || tool.startsWith("pen-") || !tool && lineShape(path?.shape);
|
||||
if (line) {
|
||||
el.shapeFill.checked = false;
|
||||
el.shapeStroke.checked = true;
|
||||
@@ -1913,6 +2067,9 @@ function updateDrawingControls() {
|
||||
}
|
||||
const disabled = !state.editing || !state.ready || state.exporting;
|
||||
el.arrowTool.disabled = disabled || !pageCan("insert");
|
||||
el.penTool.disabled = el.arrowTool.disabled;
|
||||
el.penTool.classList.toggle("active", tool.startsWith("pen-"));
|
||||
el.annotationTool.disabled = disabled;
|
||||
for (const [button, name] of [[el.drawLineButton, "line"], [el.drawRectangleButton, "rectangle"], [el.drawEllipseButton, "ellipse"]]) {
|
||||
button.disabled = disabled || !pageCan("insert");
|
||||
button.setAttribute("aria-pressed", String(tool === name));
|
||||
@@ -1953,11 +2110,15 @@ async function changeDocument(name, item, ...args) {
|
||||
ofdgoDeleteObject: "delete", ofdgoDeleteObjects: "delete", ofdgoCopyObjects: "copy", ofdgoOrderObjects: "order", ofdgoGroupObjects: "group" }[name];
|
||||
const members = item?.items || (item ? [item] : []);
|
||||
const scoped = scope && members.length && members.every(member => member.scoped);
|
||||
const inserting = ["ofdgoInsertShape", "ofdgoInsertText", "ofdgoInsertImage"].includes(name);
|
||||
const ids = item ? Array.isArray(item.id) ? item.id : members.map(member => member.id) : [];
|
||||
const annotated = !scoped && ids.some(id => id.startsWith("annotation:"));
|
||||
const inserting = ["ofdgoInsertShape", "ofdgoInsertText", "ofdgoInsertImage", "ofdgoInsertInk"].includes(name);
|
||||
const editingAnnotation = name === "ofdgoWriteAnnotation" && Boolean(state.annotationCreate?.item);
|
||||
if (scope && inserting) args.push(scope.key);
|
||||
if (scoped && !operation) throw new Error("内部对象暂不支持此操作");
|
||||
const doc = scoped
|
||||
? await callWASM("ofdgoChangeCompositeObjects", item.index, scope.key, Array.isArray(item.id) ? item.id : members.map(member => member.id), operation, ...args)
|
||||
: annotated && operation ? await callWASM("ofdgoChangeSelection", item.index, ids, operation, ...args)
|
||||
: await callWASM(name, ...(item ? [item.index, item.id] : []), ...args);
|
||||
if (openSeq !== state.openSeq) {
|
||||
return;
|
||||
@@ -1975,7 +2136,7 @@ async function changeDocument(name, item, ...args) {
|
||||
const restoredIndex = location ? doc.pages.findIndex(page => page.id === location.page) : -1;
|
||||
if (restoredIndex >= 0) {
|
||||
state.composite = location.scope ? { index: restoredIndex, key: location.scope, objects: [] } : null;
|
||||
} else if (!item && !inserting && !(name === "ofdgoPasteObjects" && args[4])) {
|
||||
} else if (!item && !inserting && !editingAnnotation && !(name === "ofdgoPasteObjects" && args[4])) {
|
||||
resetCompositeScope();
|
||||
}
|
||||
if (restoring && JSON.stringify(doc.outlines) !== JSON.stringify(state.doc.outlines)) {
|
||||
@@ -2000,17 +2161,17 @@ async function changeDocument(name, item, ...args) {
|
||||
rememberEditorView(before, revision);
|
||||
return true;
|
||||
}
|
||||
const clearSelection = !item || name === "ofdgoChangeAnnotations" && args[0] === "delete"
|
||||
const clearSelection = !item && !editingAnnotation || name === "ofdgoChangeAnnotations" && args[0] === "delete"
|
||||
|| ["ofdgoDeleteObject", "ofdgoDeleteObjects", "ofdgoEraseObjects", "ofdgoEraseObjectsPath"].includes(name);
|
||||
if (name === "ofdgoCopyObjects" || name === "ofdgoPasteObjects" || name === "ofdgoGroupObjects" || name === "ofdgoInsertShape" || name === "ofdgoInsertText" || name === "ofdgoInsertImage") {
|
||||
if (name === "ofdgoCopyObjects" || name === "ofdgoPasteObjects" || name === "ofdgoGroupObjects" || inserting || name === "ofdgoWriteAnnotation" && !editingAnnotation) {
|
||||
state.selectObjects = true;
|
||||
canvasEditor.setTool("");
|
||||
if (name !== "ofdgoInsertInk") canvasEditor.setTool("");
|
||||
setPan(false);
|
||||
canvasEditor.pendingSelection = { index: item?.index ?? args[0], ids: doc.selectedIDs };
|
||||
canvasEditor.pendingSelection = { index: item?.index ?? args[0], ids: name === "ofdgoInsertInk" ? [] : doc.selectedIDs };
|
||||
}
|
||||
if (scoped && name === "ofdgoOrderObjects") canvasEditor.pendingSelection = { index: item.index, ids: doc.selectedIDs };
|
||||
openSeq = ++state.openSeq;
|
||||
if (item || name === "ofdgoPasteObjects" || name === "ofdgoInsertShape" || name === "ofdgoInsertText" || name === "ofdgoInsertImage") {
|
||||
if (item || name === "ofdgoPasteObjects" || inserting || editingAnnotation) {
|
||||
await refreshEditorPage(doc, item ? item.index : args[0], openSeq, clearSelection);
|
||||
} else {
|
||||
const samePage = doc.pages.findIndex((page) => page.id === previous.id);
|
||||
@@ -2029,7 +2190,9 @@ async function changeDocument(name, item, ...args) {
|
||||
}
|
||||
} catch (err) {
|
||||
if (openSeq === state.openSeq) {
|
||||
if (el.annotationNote.open) {
|
||||
if (el.annotationCreate.open) {
|
||||
el.annotationCreateStatus.textContent = err.message;
|
||||
} else if (el.annotationNote.open) {
|
||||
el.annotationStatus.textContent = err.message;
|
||||
} else if (el.batchPagesPanel.open) {
|
||||
el.batchPagesStatus.textContent = err.message;
|
||||
@@ -2588,6 +2751,8 @@ async function openOFD(file) {
|
||||
el.paragraphPanel.close();
|
||||
el.infoPanel.close();
|
||||
el.annotationNote.close();
|
||||
el.annotationCreate.close();
|
||||
el.objectPicker.close();
|
||||
el.importPanel.close();
|
||||
el.batchPagesPanel.close();
|
||||
el.objectStylePanel.close();
|
||||
@@ -5800,7 +5965,7 @@ function updateObjectControls(item, reset = false) {
|
||||
el.editObjectButton.disabled = disabled || Boolean(item.items) || !canEditObject(item, "enter") && (item.type === "PathObject"
|
||||
|| !canEditObject(item, item.type === "TextObject" ? "reflow" : item.type === "ImageObject" ? "replaceImage" : "update"));
|
||||
el.editObjectButton.textContent = canEditObject(item, "enter") ? "进入" : item?.type === "ImageObject" ? "替换" : "修改";
|
||||
el.editObjectButton.title = canEditObject(item, "enter") ? "进入组合" : item?.type === "ImageObject" ? "替换图片" : item?.type === "Annotation" ? "修改注解" : "修改对象";
|
||||
el.editObjectButton.title = canEditObject(item, "enter") ? item?.type === "Annotation" ? "进入注解" : "进入组合" : item?.type === "ImageObject" ? "替换图片" : item?.type === "Annotation" ? "修改注解" : "修改对象";
|
||||
el.editObjectButton.setAttribute("aria-label", el.editObjectButton.title);
|
||||
el.multiSelectButton.disabled = !state.editing || !canvasEditor.enabled || !state.ready || state.exporting;
|
||||
const selection = selectedText(item);
|
||||
|
||||
+96
-16
@@ -178,7 +178,6 @@ function paintShape(node, shape, box) {
|
||||
export function canEditObject(item, capability) {
|
||||
if (!item) return false;
|
||||
const objects = item.items || [item];
|
||||
if (objects.some(object => object.type === "Annotation") && objects.some(object => object.type !== "Annotation")) return false;
|
||||
return objects.every(object => Boolean(object.capabilities?.[capability] || capability === "move" && object.capabilities?.transform));
|
||||
}
|
||||
|
||||
@@ -191,7 +190,6 @@ export function missingGlyphMessage(diagnostic, action = "请更换字体") {
|
||||
}
|
||||
|
||||
export function objectEditReason(item) {
|
||||
if (item?.items?.some(object => object.type === "Annotation") && item.items.some(object => object.type !== "Annotation")) return "注解与正文需分别操作";
|
||||
const labels = { fontUnavailable: "字体不可用", unsupportedColor: "暂不支持此颜色", unsupportedStyle: "暂不支持此样式",
|
||||
unsupportedContainer: "暂不支持此对象结构", invalidObject: "对象数据异常" };
|
||||
const reasons = (item?.items || (item ? [item] : [])).map(object => {
|
||||
@@ -231,6 +229,15 @@ export class CanvasEditor {
|
||||
viewer.addEventListener("pointermove", (event) => this.move(event));
|
||||
viewer.addEventListener("pointerup", (event) => this.end(event));
|
||||
viewer.addEventListener("pointercancel", () => this.cancel());
|
||||
viewer.addEventListener("contextmenu", event => {
|
||||
if (!this.enabled || this.input || this.options.busy()) return;
|
||||
const items = this.itemsAt(event);
|
||||
if (items.length > 1) {
|
||||
event.preventDefault();
|
||||
this.cancel();
|
||||
this.options.onPick?.(items);
|
||||
}
|
||||
});
|
||||
viewer.addEventListener("lostpointercapture", () => this.cancel());
|
||||
viewer.addEventListener("keydown", (event) => this.keyDown(event));
|
||||
viewer.addEventListener("keyup", (event) => this.keyUp(event));
|
||||
@@ -282,12 +289,10 @@ export class CanvasEditor {
|
||||
const layer = document.createElement("div");
|
||||
layer.className = "edit-layer";
|
||||
const artwork = new Map([...surface.querySelectorAll("[data-ofd-object], [data-ofd-child]")].map((node, order) => [node.getAttribute("data-ofd-object") || node.getAttribute("data-ofd-child"), { node, order }]));
|
||||
const objects = [...page.objects].sort((a, b) => Number(Boolean(b.background)) - Number(Boolean(a.background))
|
||||
|| (artwork.get(a.id)?.order ?? 0) - (artwork.get(b.id)?.order ?? 0));
|
||||
const objects = [...page.objects].sort((a, b) => (artwork.get(a.id)?.order ?? 0) - (artwork.get(b.id)?.order ?? 0));
|
||||
for (const object of objects) {
|
||||
const node = document.createElement("div");
|
||||
node.className = "edit-object";
|
||||
node.classList.toggle("edit-background", Boolean(object.background));
|
||||
node.classList.toggle("read-only", !canEditObject(object, "move"));
|
||||
node.title = objectEditReason(object);
|
||||
if (lineShape(object.shape)) {
|
||||
@@ -298,7 +303,7 @@ export class CanvasEditor {
|
||||
node.setAttribute("aria-label", { ImageObject: "图片对象", TextObject: "文字对象", PathObject: "图形对象", CompositeObject: "复合对象", CompositeGraphicUnit: "复合对象", Annotation: "注解对象" }[object.type]);
|
||||
const item = { ...object, index, page: { width: page.width, height: page.height }, node, surface };
|
||||
item.artwork = artwork.get(object.id)?.node;
|
||||
if (object.type === "PathObject" && (object.shape || object.outline) || (object.scoped || object.type === "CompositeObject" || object.type === "CompositeGraphicUnit") && object.contours?.length) {
|
||||
if (object.type === "PathObject" && (object.shape || object.outline) || (object.scoped || object.type === "CompositeObject" || object.type === "CompositeGraphicUnit" || object.type === "Annotation") && object.contours?.length) {
|
||||
node.classList.add("edit-contour");
|
||||
const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
|
||||
svg.classList.add("edit-outline");
|
||||
@@ -519,6 +524,7 @@ export class CanvasEditor {
|
||||
}
|
||||
if (this.tool) {
|
||||
if (this.tool.startsWith("erase-")) this.startErase(event);
|
||||
else if (this.tool.startsWith("pen-")) this.startInk(event);
|
||||
else this.startShape(event);
|
||||
return;
|
||||
}
|
||||
@@ -533,9 +539,8 @@ export class CanvasEditor {
|
||||
node = target?.node;
|
||||
this.focus();
|
||||
const additive = event.shiftKey || event.ctrlKey || event.metaKey || this.multiple;
|
||||
if (!target || target.background && !this.items().includes(target)) {
|
||||
if (!target) {
|
||||
this.startMarquee(event, additive);
|
||||
if (this.drag?.marquee) this.drag.target = target;
|
||||
return;
|
||||
}
|
||||
if (additive && !event.target.dataset.corner) {
|
||||
@@ -587,6 +592,10 @@ export class CanvasEditor {
|
||||
return [...surface.querySelectorAll(".edit-object")].map(node => this.nodes.get(node)).reverse().filter(item => this.contains(item, point, tolerance));
|
||||
}
|
||||
|
||||
pageItems(index) {
|
||||
return [...this.viewer.querySelectorAll(".edit-object")].map(node => this.nodes.get(node)).filter(item => item?.index === index);
|
||||
}
|
||||
|
||||
cycleSelection(event) {
|
||||
const items = this.itemsAt(event);
|
||||
if (!items.length) return;
|
||||
@@ -602,6 +611,10 @@ export class CanvasEditor {
|
||||
}
|
||||
event.preventDefault();
|
||||
drag.pointer = event;
|
||||
if (drag.ink) {
|
||||
this.moveInk(event, drag);
|
||||
return;
|
||||
}
|
||||
if (drag.erase) {
|
||||
this.moveErase(event, drag);
|
||||
return;
|
||||
@@ -729,6 +742,16 @@ export class CanvasEditor {
|
||||
return;
|
||||
}
|
||||
this.move(event);
|
||||
if (drag.ink) {
|
||||
this.drag = null;
|
||||
this.viewer.releasePointerCapture(drag.pointerID);
|
||||
if (drag.pressure && drag.points.every(point => point.pressure === 0)) {
|
||||
drag.preview.remove();
|
||||
return;
|
||||
}
|
||||
Promise.resolve(this.options.onDrawInk(drag.page.index, drag.points, drag.style, drag.pressure)).finally(() => drag.preview.remove());
|
||||
return;
|
||||
}
|
||||
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
|
||||
@@ -746,12 +769,19 @@ export class CanvasEditor {
|
||||
this.drag = null;
|
||||
drag.preview.remove();
|
||||
this.viewer.releasePointerCapture(drag.pointerID);
|
||||
if (drag.target && !drag.moved) this.setSelection([...drag.base, drag.target]);
|
||||
return;
|
||||
}
|
||||
if (drag.shape) {
|
||||
this.drag = null;
|
||||
this.viewer.releasePointerCapture(drag.pointerID);
|
||||
if (drag.annotation) {
|
||||
const point = pagePoint(drag.clientX, drag.clientY, drag.rect, drag.page, drag.rotation);
|
||||
const box = drag.box || { ...point, width: drag.annotation === "note" ? 5 : 40, height: drag.annotation === "note" ? 5 : 12 };
|
||||
drag.preview.remove();
|
||||
this.setTool("");
|
||||
this.options.onDrawAnnotation(drag.page.index, drag.annotation, box);
|
||||
return;
|
||||
}
|
||||
if (drag.shape === "text") {
|
||||
const from = pagePoint(drag.clientX, drag.clientY, drag.rect, drag.page, drag.rotation);
|
||||
const x = Math.max(0, Math.min(from.x, drag.page.width - 1));
|
||||
@@ -812,14 +842,56 @@ export class CanvasEditor {
|
||||
}
|
||||
}
|
||||
|
||||
startInk(event) {
|
||||
const surface = event.target.closest(".page-surface") || this.nearestSurface(event), page = this.pages.get(surface);
|
||||
if (!page || !this.options.canInsert(page.index)) return;
|
||||
event.preventDefault();
|
||||
this.focus();
|
||||
const style = { ...this.options.drawStyle(), fill: false, stroke: true };
|
||||
const pressure = this.tool === "pen-pressure";
|
||||
const preview = this.createPreview(surface, page, "path", pressure ? { ...style, fill: true, fillColor: style.strokeColor, stroke: false } : style);
|
||||
preview.node.setAttribute("stroke-linecap", "round");
|
||||
preview.node.setAttribute("stroke-linejoin", "round");
|
||||
this.drag = { ink: true, pressure, points: [], parts: [], page, surface, style, ...preview, pointerID: event.pointerId,
|
||||
rect: surface.getBoundingClientRect(), rotation: this.options.rotation() };
|
||||
this.viewer.setPointerCapture(event.pointerId);
|
||||
this.moveInk(event, this.drag);
|
||||
}
|
||||
|
||||
moveInk(event, drag) {
|
||||
const coalesced = event.getCoalescedEvents?.() || [];
|
||||
for (const sample of coalesced.length ? coalesced : [event]) {
|
||||
const point = pagePoint(sample.clientX, sample.clientY, drag.rect, drag.page, drag.rotation);
|
||||
const last = drag.points.at(-1);
|
||||
point.pressure = sample.pointerType === "pen" ? event.type === "pointerup" ? last?.pressure ?? sample.pressure : sample.pressure : 0.5;
|
||||
if (last && Math.hypot(point.x-last.x, point.y-last.y) < 0.05 && Math.abs(point.pressure-last.pressure) < 0.01) continue;
|
||||
drag.points.push(point);
|
||||
if (!drag.pressure) {
|
||||
drag.parts.push(`${last ? "L" : "M"}${point.x} ${point.y}`);
|
||||
} else {
|
||||
const radius = drag.style.lineWidth * point.pressure / 2;
|
||||
if (radius > 0) drag.parts.push(`M${point.x+radius} ${point.y}a${radius} ${radius} 0 1 1 ${-2*radius} 0a${radius} ${radius} 0 1 1 ${2*radius} 0Z`);
|
||||
if (last) {
|
||||
const r = drag.style.lineWidth * last.pressure / 2, dx = point.x-last.x, dy = point.y-last.y, length = Math.hypot(dx,dy);
|
||||
if ((radius > 0 || r > 0) && length > Math.abs(radius-r)) {
|
||||
const x = dx/length, y = dy/length, a = (r-radius)/length, b = Math.sqrt(1-a*a);
|
||||
const nx = a*x-b*y, ny = a*y+b*x, mx = a*x+b*y, my = a*y-b*x;
|
||||
drag.parts.push(`M${last.x+r*mx} ${last.y+r*my}L${point.x+radius*mx} ${point.y+radius*my}L${point.x+radius*nx} ${point.y+radius*ny}L${last.x+r*nx} ${last.y+r*ny}Z`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
const dot = drag.points.length === 1 && !drag.pressure ? `l0.001 0` : "";
|
||||
drag.node.setAttribute("d", drag.parts.join(" ") + dot);
|
||||
}
|
||||
|
||||
startErase(event) {
|
||||
const surface = event.target.closest(".page-surface") || this.nearestSurface(event);
|
||||
const page = this.pages.get(surface);
|
||||
if (!page) return;
|
||||
event.preventDefault();
|
||||
this.focus();
|
||||
this.drag = { erase: this.tool, page, surface, items: [...surface.querySelectorAll(".edit-object")].map(node => this.nodes.get(node))
|
||||
.filter(item => this.tool === "erase-object" || !item.background),
|
||||
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",
|
||||
@@ -875,12 +947,10 @@ export class CanvasEditor {
|
||||
const box = { x: Math.min(from.x, to.x), y: Math.min(from.y, to.y), width: Math.abs(to.x-from.x), height: Math.abs(to.y-from.y) };
|
||||
paintShape(drag.node, "rectangle", box);
|
||||
if (Math.hypot(event.clientX-drag.clientX, event.clientY-drag.clientY) < 3) return;
|
||||
drag.moved = true;
|
||||
const items = [...drag.base];
|
||||
for (const node of drag.surface.querySelectorAll(".edit-object")) {
|
||||
const item = this.nodes.get(node);
|
||||
if (item.background && (item.x < box.x || item.y < box.y || item.x + item.width > box.x + box.width || item.y + item.height > box.y + box.height)) continue;
|
||||
if (!items.includes(item) && item.x <= box.x+box.width && item.x+item.width >= box.x && item.y <= box.y+box.height && item.y+item.height >= box.y) {
|
||||
if (!items.includes(item) && item.x >= box.x && item.x+item.width <= box.x+box.width && item.y >= box.y && item.y+item.height <= box.y+box.height) {
|
||||
items.push(item);
|
||||
}
|
||||
}
|
||||
@@ -895,13 +965,14 @@ export class CanvasEditor {
|
||||
}
|
||||
event.preventDefault();
|
||||
this.focus();
|
||||
const shape = this.tool;
|
||||
const annotation = this.tool.startsWith("annotation:") ? this.tool.slice(11) : "";
|
||||
const shape = annotation ? "rectangle" : this.tool;
|
||||
const style = this.options.drawStyle();
|
||||
if (lineShape(shape)) {
|
||||
style.fill = false;
|
||||
style.stroke = true;
|
||||
}
|
||||
this.drag = { shape, page, surface, ...this.createPreview(surface, page, shape === "text" ? "rectangle" : shape, style), style, pointerID: event.pointerId,
|
||||
this.drag = { shape, annotation, page, surface, ...this.createPreview(surface, page, shape === "text" ? "rectangle" : shape, style), style, pointerID: event.pointerId,
|
||||
rect: surface.getBoundingClientRect(), rotation: this.options.rotation(),
|
||||
clientX: event.clientX, clientY: event.clientY };
|
||||
this.viewer.setPointerCapture(event.pointerId);
|
||||
@@ -1026,6 +1097,15 @@ export class CanvasEditor {
|
||||
if (this.input || !this.enabled || !this.selected || this.options.busy() || this.nudgeCommit || event.ctrlKey || event.metaKey || event.altKey || event.isComposing) {
|
||||
return;
|
||||
}
|
||||
if (event.key === "Tab" && !this.drag && !this.nudge) {
|
||||
const items = this.pageItems(this.selected.index);
|
||||
if (items.length) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
this.select(items[(items.indexOf(this.selected) + (event.shiftKey ? -1 : 1) + items.length) % items.length]);
|
||||
}
|
||||
return;
|
||||
}
|
||||
const directions = { ArrowLeft: [-1, 0], ArrowRight: [1, 0], ArrowUp: [0, -1], ArrowDown: [0, 1] };
|
||||
const edit = event.key === "Enter" || event.key === "F2";
|
||||
if (event.key !== "Escape" && event.key !== "Delete" && !edit && !directions[event.key]) {
|
||||
|
||||
Reference in new issue
Block a user