svgedit
Version:
Powerful SVG-Editor for your browser
298 lines (297 loc) • 9.63 kB
JavaScript
//#region src/editor/extensions/ext-markers/ext-markers.js
/**
* @file ext-markers.js
*
* @license Apache-2.0
*
* @copyright 2010 Will Schleter based on ext-arrows.js by Copyright(c) 2010 Alexis Deveria
* @copyright 2021 OptimistikSAS
*
* This extension provides for the addition of markers to the either end
* or the middle of a line, polyline, path, polygon.
*
* Markers are graphics
*
* to simplify the coding and make the implementation as robust as possible,
* markers are not shared - every object has its own set of markers.
* this relationship is maintained by a naming convention between the
* ids of the markers and the ids of the object
*
* The following restrictions exist for simplicty of use and programming
* objects and their markers to have the same color
* marker size is fixed
* an application specific attribute - se_type - is added to each marker element
* to store the type of marker
*
* @todo
* remove some of the restrictions above
*
*/
var ext_markers_default = {
name: "markers",
async init() {
const svgEditor = this;
const { svgCanvas } = svgEditor;
const { BatchCommand, RemoveElementCommand, InsertElementCommand } = svgCanvas.history;
const { $id, addSVGElementsFromJson: addElem } = svgCanvas;
const mtypes = [
"start",
"mid",
"end"
];
const markerElems = [
"line",
"path",
"polyline",
"polygon"
];
const markerTypes = {
nomarker: {},
leftarrow: {
element: "path",
attr: { d: "M0,50 L100,90 L70,50 L100,10 Z" }
},
rightarrow: {
element: "path",
attr: { d: "M100,50 L0,90 L30,50 L0,10 Z" }
},
box: {
element: "path",
attr: { d: "M20,20 L20,80 L80,80 L80,20 Z" }
},
mcircle: {
element: "circle",
attr: {
r: 30,
cx: 50,
cy: 50
}
}
};
[
"leftarrow",
"rightarrow",
"box",
"mcircle"
].forEach((v) => {
markerTypes[v + "_o"] = markerTypes[v];
});
/**
* @param {Element} elem - A graphic element will have an attribute like marker-start
* @param {"marker-start"|"marker-mid"|"marker-end"} attr
* @returns {Element} The marker element that is linked to the graphic element
*/
const getLinked = (elem, attr) => {
const str = elem.getAttribute(attr);
if (!str) return null;
const m = str.match(/\(#(.*)\)/);
if (!m || m.length !== 2) return null;
return svgCanvas.getElement(m[1]);
};
/**
* Toggles context tool panel off/on.
* @param {boolean} on
* @returns {void}
*/
const showPanel = (on, elem) => {
$id("marker_panel").style.display = on ? "block" : "none";
if (on && elem) mtypes.forEach((pos) => {
const marker = getLinked(elem, "marker-" + pos);
if (marker?.attributes?.se_type) $id(`${pos}_marker_list_opts`).setAttribute("value", marker.attributes.se_type.value);
else $id(`${pos}_marker_list_opts`).setAttribute("value", "nomarker");
});
};
/**
* @param {string} id
* @param {""|"nomarker"|"nomarker"|"leftarrow"|"rightarrow"|"textmarker"|"forwardslash"|"reverseslash"|"verticalslash"|"box"|"star"|"xmark"|"triangle"|"mcircle"} seType
* @returns {SVGMarkerElement}
*/
const addMarker = (id, seType) => {
const selElems = svgCanvas.getSelectedElements();
let marker = svgCanvas.getElement(id);
if (marker) return;
if (seType === "" || seType === "nomarker") return;
const color = selElems[0].getAttribute("stroke");
const strokeWidth = 10;
const refX = 50;
const refY = 50;
const viewBox = "0 0 100 100";
const markerWidth = 5;
const markerHeight = 5;
if (!markerTypes[seType]) {
console.error(`unknown marker type: ${seType}`);
return;
}
marker = addElem({
element: "marker",
attr: {
id,
markerUnits: "strokeWidth",
orient: "auto",
style: "pointer-events:none",
se_type: seType
}
});
const mel = addElem(markerTypes[seType]);
const fillcolor = seType.substr(-2) === "_o" ? "none" : color;
mel.setAttribute("fill", fillcolor);
mel.setAttribute("stroke", color);
mel.setAttribute("stroke-width", strokeWidth);
marker.append(mel);
marker.setAttribute("viewBox", viewBox);
marker.setAttribute("markerWidth", markerWidth);
marker.setAttribute("markerHeight", markerHeight);
marker.setAttribute("refX", refX);
marker.setAttribute("refY", refY);
svgCanvas.findDefs().append(marker);
return marker;
};
/**
* @param {Element} elem
* @returns {SVGPolylineElement}
*/
const convertline = (elem) => {
if (elem.tagName !== "line") return elem;
const x1 = Number(elem.getAttribute("x1"));
const x2 = Number(elem.getAttribute("x2"));
const y1 = Number(elem.getAttribute("y1"));
const y2 = Number(elem.getAttribute("y2"));
const { id } = elem;
const midPt = " " + (x1 + x2) / 2 + "," + (y1 + y2) / 2 + " ";
const pline = addElem({
element: "polyline",
attr: {
points: x1 + "," + y1 + midPt + x2 + "," + y2,
stroke: elem.getAttribute("stroke"),
"stroke-width": elem.getAttribute("stroke-width"),
fill: "none",
opacity: elem.getAttribute("opacity") || 1
}
});
mtypes.forEach((pos) => {
const nam = "marker-" + pos;
if (elem.getAttribute(nam)) pline.setAttribute(nam, elem.getAttribute(nam));
});
const batchCmd = new BatchCommand();
batchCmd.addSubCommand(new RemoveElementCommand(elem, elem.parentNode));
batchCmd.addSubCommand(new InsertElementCommand(pline));
elem.insertAdjacentElement("afterend", pline);
elem.remove();
svgCanvas.clearSelection();
pline.id = id;
svgCanvas.addToSelection([pline]);
svgCanvas.addCommandToHistory(batchCmd);
return pline;
};
/**
*
* @returns {void}
*/
const setMarker = (pos, markerType) => {
const selElems = svgCanvas.getSelectedElements();
if (selElems.length === 0) return;
const markerName = "marker-" + pos;
const el = selElems[0];
const marker = getLinked(el, markerName);
if (marker) marker.remove();
el.removeAttribute(markerName);
let val = markerType;
if (val === "") val = "nomarker";
if (val === "nomarker") {
svgCanvas.call("changed", selElems);
return;
}
const id = "mkr_" + pos + "_" + el.id;
addMarker(id, val);
svgCanvas.changeSelectedAttribute(markerName, "url(#" + id + ")");
if (el.tagName === "line" && pos === "mid") convertline(el);
svgCanvas.call("changed", selElems);
};
/**
* Called when the main system modifies an object. This routine changes
* the associated markers to be the same color.
* @param {Element} elem
* @returns {void}
*/
const colorChanged = (elem) => {
const color = elem.getAttribute("stroke");
mtypes.forEach((pos) => {
const marker = getLinked(elem, "marker-" + pos);
if (!marker) return;
if (!marker.attributes.se_type) return;
const ch = marker.lastElementChild;
if (!ch) return;
const curfill = ch.getAttribute("fill");
const curstroke = ch.getAttribute("stroke");
if (curfill && curfill !== "none") ch.setAttribute("fill", color);
if (curstroke && curstroke !== "none") ch.setAttribute("stroke", color);
});
};
/**
* Called when the main system creates or modifies an object.
* Its primary purpose is to create new markers for cloned objects.
* @param {Element} el
* @returns {void}
*/
const updateReferences = (el) => {
const selElems = svgCanvas.getSelectedElements();
mtypes.forEach((pos) => {
const markerName = "marker-" + pos;
const marker = getLinked(el, markerName);
if (!marker || !marker.attributes.se_type) return;
const url = el.getAttribute(markerName);
if (url) {
const len = el.id.length;
const linkid = url.substr(-len - 1, len);
if (el.id !== linkid) {
const newMarkerId = "mkr_" + pos + "_" + el.id;
addMarker(newMarkerId, marker.attributes.se_type.value);
svgCanvas.changeSelectedAttribute(markerName, "url(#" + newMarkerId + ")");
svgCanvas.call("changed", selElems);
}
}
});
};
return {
name: svgEditor.i18next.t(`${name}:name`),
callback() {
const panelTemplate = document.createElement("template");
let innerHTML = "<div id=\"marker_panel\">";
mtypes.forEach((pos) => {
innerHTML += `<se-list id="${pos}_marker_list_opts" title="tools.${pos}_marker_list_opts" label="" width="22px" height="22px">`;
Object.entries(markerTypes).forEach(([marker, _mkr]) => {
innerHTML += `<se-list-item id="mkr_${pos}_${marker}" value="${marker}" title="tools.mkr_${marker}" src="${marker}.svg" img-height="22px"></se-list-item>`;
});
innerHTML += "</se-list>";
});
innerHTML += "</div>";
panelTemplate.innerHTML = innerHTML;
$id("tools_top").appendChild(panelTemplate.content.cloneNode(true));
showPanel(false);
mtypes.forEach((pos) => {
$id(`${pos}_marker_list_opts`).addEventListener("change", (evt) => {
setMarker(pos, evt.detail.value);
});
});
},
selectedChanged(opts) {
if (opts.elems.length === 0) showPanel(false);
opts.elems.forEach((elem) => {
if (elem && markerElems.includes(elem.tagName)) if (opts.selectedElement && !opts.multiselected) showPanel(true, elem);
else showPanel(false);
else showPanel(false);
});
},
elementChanged(opts) {
const elem = opts.elems[0];
if (elem && (elem.getAttribute("marker-start") || elem.getAttribute("marker-mid") || elem.getAttribute("marker-end"))) {
colorChanged(elem);
updateReferences(elem);
}
}
};
}
};
//#endregion
export { ext_markers_default as default };
//# sourceMappingURL=ext-markers.js.map