Files
svgedit/packages/svgcanvas/core/elem-get-set.js
shanyue f6fbc8d9ff
Some checks failed
CodeQL / Analyze (javascript) (push) Has been cancelled
check each push / build (push) Has been cancelled
feat(svgcanvas): Trigger 'changed' event on text attribute modifications (#1085)
* feat(svgcanvas): Trigger 'changed' event on text attribute modifications

This update adds a call to the 'changed' event for various text attribute methods, ensuring that changes to text elements are properly tracked and reflected in the SVG canvas. The methods updated include setBold, setItalic, setTextAnchor, setLetterSpacing, setWordSpacing, setTextLength, setLengthAdjust, setFontFamily, setFontColor, and setFontSize.

* refactor(svgcanvas): Simplify text attribute methods and optimize 'changed' event emission

This update introduces helper functions to streamline the handling of selected text elements and their attribute changes. The methods setBold, setItalic, setTextAnchor, setLetterSpacing, setWordSpacing, and setTextLength now utilize these helpers to filter and notify only modified text elements, improving performance and clarity in event emissions.
2026-03-20 00:23:35 +01:00

1200 lines
39 KiB
JavaScript

/**
* @module elem-get-set get and set methods.
* @license MIT
* @copyright 2011 Jeff Schiller
*/
import Paint from './paint.js'
import { NS } from './namespaces.js'
import {
getVisibleElements, getStrokedBBoxDefaultVisible, findDefs,
walkTree, getHref, setHref, getElement
} from './utilities.js'
import {
convertToNum
} from './units.js'
import { getParents } from '../common/util.js'
let svgCanvas = null
/**
* @function module:elem-get-set.init
* @param {module:elem-get-set.elemContext} elemContext
* @returns {void}
*/
export const init = (canvas) => {
svgCanvas = canvas
svgCanvas.getBold = getBoldMethod // Check whether selected element is bold or not.
svgCanvas.setBold = setBoldMethod // Make the selected element bold or normal.
svgCanvas.getItalic = getItalicMethod // Check whether selected element is in italics or not.
svgCanvas.setItalic = setItalicMethod // Make the selected element italic or normal.
svgCanvas.hasTextDecoration = hasTextDecorationMethod // Check whether the selected element has the given text decoration or not.
svgCanvas.addTextDecoration = addTextDecorationMethod // Adds the given value to the text decoration
svgCanvas.removeTextDecoration = removeTextDecorationMethod // Removes the given value from the text decoration
svgCanvas.setTextAnchor = setTextAnchorMethod // Set the new text anchor.
svgCanvas.setLetterSpacing = setLetterSpacingMethod // Set the new letter spacing.
svgCanvas.setWordSpacing = setWordSpacingMethod // Set the new word spacing.
svgCanvas.setTextLength = setTextLengthMethod // Set the new text length.
svgCanvas.setLengthAdjust = setLengthAdjustMethod // Set the new length adjust.
svgCanvas.getFontFamily = getFontFamilyMethod // The current font family
svgCanvas.setFontFamily = setFontFamilyMethod // Set the new font family.
svgCanvas.setFontColor = setFontColorMethod // Set the new font color.
svgCanvas.getFontColor = getFontColorMethod // The current font color
svgCanvas.getFontSize = getFontSizeMethod // The current font size
svgCanvas.setFontSize = setFontSizeMethod // Applies the given font size to the selected element.
svgCanvas.getText = getTextMethod // current text (`textContent`) of the selected element
svgCanvas.setTextContent = setTextContentMethod // Updates the text element with the given string.
svgCanvas.setImageURL = setImageURLMethod // Sets the new image URL for the selected image element
svgCanvas.setLinkURL = setLinkURLMethod // Sets the new link URL for the selected anchor element.
svgCanvas.setRectRadius = setRectRadiusMethod // Sets the `rx` and `ry` values to the selected `rect` element
svgCanvas.makeHyperlink = makeHyperlinkMethod // Wraps the selected element(s) in an anchor element or converts group to one.
svgCanvas.removeHyperlink = removeHyperlinkMethod
svgCanvas.setSegType = setSegTypeMethod // Sets the new segment type to the selected segment(s).
svgCanvas.setStrokeWidth = setStrokeWidthMethod // Sets the stroke width for the current selected elements.
svgCanvas.getResolution = getResolutionMethod // The current dimensions and zoom level in an object
svgCanvas.getTitle = getTitleMethod // the current group/SVG's title contents or `undefined` if no element
svgCanvas.setGroupTitle = setGroupTitleMethod // Sets the group/SVG's title content.
svgCanvas.setStrokeAttr = setStrokeAttrMethod // Set the given stroke-related attribute the given value for selected elements.
svgCanvas.setBackground = setBackgroundMethod // Set the background of the editor (NOT the actual document).
svgCanvas.setDocumentTitle = setDocumentTitleMethod // Adds/updates a title element for the document with the given name.
svgCanvas.getEditorNS = getEditorNSMethod // Returns the editor's namespace URL, optionally adding it to the root element.
svgCanvas.setResolution = setResolutionMethod // Changes the document's dimensions to the given size.
svgCanvas.setBBoxZoom = setBBoxZoomMethod // Sets the zoom level on the canvas-side based on the given value.
svgCanvas.setCurrentZoom = setZoomMethod // Sets the zoom to the given level.
svgCanvas.setColor = setColorMethod // Change the current stroke/fill color/gradien
svgCanvas.setGradient = setGradientMethod // Apply the current gradient to selected element's fill or stroke.
svgCanvas.setPaint = setPaintMethod // Set a color/gradient to a fill/stroke.
}
/**
* @function module:elem-get-set.SvgCanvas#getResolution
* @returns {DimensionsAndZoom} The current dimensions and zoom level in an object
*/
const getResolutionMethod = () => {
const zoom = svgCanvas.getZoom()
const w = svgCanvas.getSvgContent().getAttribute('width') / zoom
const h = svgCanvas.getSvgContent().getAttribute('height') / zoom
return {
w,
h,
zoom
}
}
/**
* @function module:elem-get-set.SvgCanvas#getTitle
* @param {Element} [elem]
* @returns {string|void} the current group/SVG's title contents or
* `undefined` if no element is passed nd there are no selected elements.
*/
const getTitleMethod = (elem) => {
const selectedElements = svgCanvas.getSelectedElements()
const dataStorage = svgCanvas.getDataStorage()
elem = elem || selectedElements[0]
if (!elem) { return undefined }
if (dataStorage.has(elem, 'gsvg')) {
elem = dataStorage.get(elem, 'gsvg')
} else if (dataStorage.has(elem, 'symbol')) {
elem = dataStorage.get(elem, 'symbol')
}
const childs = elem.childNodes
for (const child of childs) {
if (child.nodeName === 'title') {
return child.textContent
}
}
return ''
}
/**
* Sets the group/SVG's title content.
* @function module:elem-get-set.SvgCanvas#setGroupTitle
* @param {string} val
* @todo Combine this with `setDocumentTitle`
* @returns {void}
*/
const setGroupTitleMethod = (val) => {
const {
InsertElementCommand, RemoveElementCommand,
ChangeElementCommand, BatchCommand
} = svgCanvas.history
const selectedElements = svgCanvas.getSelectedElements()
const dataStorage = svgCanvas.getDataStorage()
let elem = selectedElements[0]
if (!elem) { return }
if (dataStorage.has(elem, 'gsvg')) {
elem = dataStorage.get(elem, 'gsvg')
} else if (dataStorage.has(elem, 'symbol')) {
elem = dataStorage.get(elem, 'symbol')
}
if (!elem) { return }
const batchCmd = new BatchCommand('Set Label')
let title = null
for (const child of elem.childNodes) {
if (child.nodeName === 'title') {
title = child
break
}
}
if (val.length === 0) {
if (!title) { return }
// Remove title element
const { nextSibling } = title
title.remove()
batchCmd.addSubCommand(new RemoveElementCommand(title, nextSibling, elem))
} else if (title) {
// Change title contents
const oldText = title.textContent
if (oldText === val) { return }
title.textContent = val
batchCmd.addSubCommand(new ChangeElementCommand(title, { '#text': oldText }))
} else {
// Add title element
title = svgCanvas.getDOMDocument().createElementNS(NS.SVG, 'title')
title.textContent = val
elem.insertBefore(title, elem.firstChild)
batchCmd.addSubCommand(new InsertElementCommand(title))
}
if (!batchCmd.isEmpty()) {
svgCanvas.addCommandToHistory(batchCmd)
}
}
/**
* Adds/updates a title element for the document with the given name.
* This is an undoable action.
* @function module:elem-get-set.SvgCanvas#setDocumentTitle
* @param {string} newTitle - String with the new title
* @returns {void}
*/
const setDocumentTitleMethod = (newTitle) => {
const {
InsertElementCommand, RemoveElementCommand,
ChangeElementCommand, BatchCommand
} = svgCanvas.history
const svgContent = svgCanvas.getSvgContent()
const batchCmd = new BatchCommand('Change Image Title')
/** @type {Element|null} */
let docTitle = null
for (const child of svgContent.childNodes) {
if (child.nodeName === 'title') {
docTitle = child
break
}
}
if (!docTitle) {
if (!newTitle.length) { return }
docTitle = svgCanvas.getDOMDocument().createElementNS(NS.SVG, 'title')
docTitle.textContent = newTitle
svgContent.insertBefore(docTitle, svgContent.firstChild)
batchCmd.addSubCommand(new InsertElementCommand(docTitle))
} else if (newTitle.length) {
const oldTitle = docTitle.textContent
if (oldTitle === newTitle) { return }
docTitle.textContent = newTitle
batchCmd.addSubCommand(new ChangeElementCommand(docTitle, { '#text': oldTitle }))
} else {
// No title given, so element is not necessary
const { nextSibling } = docTitle
docTitle.remove()
batchCmd.addSubCommand(new RemoveElementCommand(docTitle, nextSibling, svgContent))
}
if (!batchCmd.isEmpty()) {
svgCanvas.addCommandToHistory(batchCmd)
}
}
/**
* Changes the document's dimensions to the given size.
* @function module:elem-get-set.SvgCanvas#setResolution
* @param {Float|"fit"} x - Number with the width of the new dimensions in user units.
* Can also be the string "fit" to indicate "fit to content".
* @param {Float} y - Number with the height of the new dimensions in user units.
* @fires module:elem-get-set.SvgCanvas#event:changed
* @returns {boolean} Indicates if resolution change was successful.
* It will fail on "fit to content" option with no content to fit to.
*/
const setResolutionMethod = (x, y) => {
const { ChangeElementCommand, BatchCommand } = svgCanvas.history
const res = svgCanvas.getResolution()
const { w, h } = res
let batchCmd
if (x === 'fit') {
// Get bounding box
const bbox = getStrokedBBoxDefaultVisible()
if (bbox) {
batchCmd = new BatchCommand('Fit Canvas to Content')
const visEls = getVisibleElements()
svgCanvas.addToSelection(visEls)
const dx = []; const dy = []
visEls.forEach((_item, _i) => {
dx.push(bbox.x * -1)
dy.push(bbox.y * -1)
})
const cmd = svgCanvas.moveSelectedElements(dx, dy, false)
if (cmd) {
batchCmd.addSubCommand(cmd)
}
svgCanvas.clearSelection()
x = Math.round(bbox.width)
y = Math.round(bbox.height)
} else {
return false
}
}
const newW = convertToNum('width', x)
const newH = convertToNum('height', y)
if (newW !== w || newH !== h) {
if (!batchCmd) {
batchCmd = new BatchCommand('Change Image Dimensions')
}
const svgContent = svgCanvas.getSvgContent()
const oldViewBox = svgContent.getAttribute('viewBox')
svgContent.setAttribute('width', newW)
svgContent.setAttribute('height', newH)
svgCanvas.contentW = newW
svgCanvas.contentH = newH
svgContent.setAttribute('viewBox', [0, 0, newW, newH].join(' '))
batchCmd.addSubCommand(new ChangeElementCommand(svgContent, { width: w, height: h, viewBox: oldViewBox }))
svgCanvas.addCommandToHistory(batchCmd)
svgCanvas.call('changed', [svgContent])
}
return true
}
/**
* Returns the editor's namespace URL, optionally adding it to the root element.
* @function module:elem-get-set.SvgCanvas#getEditorNS
* @param {boolean} [add] - Indicates whether or not to add the namespace value
* @returns {string} The editor's namespace URL
*/
const getEditorNSMethod = (add) => {
if (add) {
svgCanvas.getSvgContent().setAttribute('xmlns:se', NS.SE)
}
return NS.SE
}
/**
* @typedef {PlainObject} module:elem-get-set.ZoomAndBBox
* @property {Float} zoom
* @property {module:utilities.BBoxObject} bbox
*/
/**
* Sets the zoom level on the canvas-side based on the given value.
* @function module:elem-get-set.SvgCanvas#setBBoxZoom
* @param {"selection"|"canvas"|"content"|"layer"|module:SVGEditor.BBoxObjectWithFactor} val - Bounding box object to zoom to or string indicating zoom option. Note: the object value type is defined in `svg-editor.js`
* @param {Integer} editorW - The editor's workarea box's width
* @param {Integer} editorH - The editor's workarea box's height
* @returns {module:elem-get-set.ZoomAndBBox|void}
*/
const setBBoxZoomMethod = (val, editorW, editorH) => {
const zoom = svgCanvas.getZoom()
const selectedElements = svgCanvas.getSelectedElements()
let spacer = 0.85
let bb
const calcZoom = (bb) => {
if (!bb) { return undefined }
if (!Number.isFinite(editorW) || !Number.isFinite(editorH) || editorW <= 0 || editorH <= 0) {
return undefined
}
if (!Number.isFinite(bb.width) || !Number.isFinite(bb.height) || bb.width <= 0 || bb.height <= 0) {
return undefined
}
const wZoom = Math.round((editorW / bb.width) * 100 * spacer) / 100
const hZoom = Math.round((editorH / bb.height) * 100 * spacer) / 100
const zoom = Math.min(wZoom, hZoom)
if (!Number.isFinite(zoom) || zoom <= 0) {
return undefined
}
svgCanvas.setZoom(zoom)
return { zoom, bbox: bb }
}
if (val && typeof val === 'object') {
bb = val
if (bb.width === 0 || bb.height === 0) {
let newzoom = zoom
if (Number.isFinite(bb.zoom) && bb.zoom > 0) {
newzoom = bb.zoom
} else if (Number.isFinite(bb.factor) && bb.factor > 0) {
newzoom = zoom * bb.factor
}
if (Number.isFinite(newzoom) && newzoom > 0) {
svgCanvas.setZoom(newzoom)
}
return { zoom: newzoom, bbox: bb }
}
return calcZoom(bb)
}
switch (val) {
case 'selection': {
if (!selectedElements[0]) { return undefined }
const selectedElems = selectedElements.filter(Boolean)
bb = getStrokedBBoxDefaultVisible(selectedElems)
break
} case 'canvas': {
const res = svgCanvas.getResolution()
spacer = 0.95
bb = { width: res.w, height: res.h, x: 0, y: 0 }
break
} case 'content':
bb = getStrokedBBoxDefaultVisible()
break
case 'layer':
bb = getStrokedBBoxDefaultVisible(getVisibleElements(svgCanvas.getCurrentDrawing().getCurrentLayer()))
break
default:
return undefined
}
return calcZoom(bb)
}
/**
* Sets the zoom to the given level.
* @function module:elem-get-set.SvgCanvas#setZoom
* @param {Float} zoomLevel - Float indicating the zoom level to change to
* @fires module:elem-get-set.SvgCanvas#event:ext_zoomChanged
* @returns {void}
*/
const setZoomMethod = (zoomLevel) => {
if (!Number.isFinite(zoomLevel) || zoomLevel <= 0) {
return
}
const selectedElements = svgCanvas.getSelectedElements()
const res = svgCanvas.getResolution()
const w = res.w / zoomLevel
const h = res.h / zoomLevel
if (!Number.isFinite(w) || !Number.isFinite(h) || w <= 0 || h <= 0) {
return
}
svgCanvas.getSvgContent().setAttribute('viewBox', `0 0 ${w} ${h}`)
svgCanvas.setZoom(zoomLevel)
selectedElements.forEach((elem) => {
if (!elem) { return }
const selector = svgCanvas.selectorManager.requestSelector(elem)
selector && selector.resize()
})
svgCanvas.pathActions.zoomChange()
svgCanvas.runExtensions('zoomChanged', zoomLevel)
}
/**
* Change the current stroke/fill color/gradient value.
* @function module:elem-get-set.SvgCanvas#setColor
* @param {string} type - String indicating fill or stroke
* @param {string} val - The value to set the stroke attribute to
* @param {boolean} preventUndo - Boolean indicating whether or not svgCanvas should be an undoable option
* @fires module:elem-get-set.SvgCanvas#event:changed
* @returns {void}
*/
const setColorMethod = (type, val, preventUndo) => {
const selectedElements = svgCanvas.getSelectedElements()
svgCanvas.setCurShape(type, val)
svgCanvas.setCurProperties(`${type}_paint`, { type: 'solidColor' })
const elems = []
/**
*
* @param {Element} e
* @returns {void}
*/
const addNonG = (e) => {
if (e.nodeName !== 'g') {
elems.push(e)
}
}
let i = selectedElements.length
while (i--) {
const elem = selectedElements[i]
if (elem) {
if (elem.tagName === 'g') {
walkTree(elem, addNonG)
} else if (type === 'fill') {
if (elem.tagName !== 'polyline' && elem.tagName !== 'line') {
elems.push(elem)
}
} else {
elems.push(elem)
}
}
}
if (elems.length > 0) {
if (!preventUndo) {
svgCanvas.changeSelectedAttribute(type, val, elems)
svgCanvas.call('changed', elems)
} else {
svgCanvas.changeSelectedAttributeNoUndo(type, val, elems)
}
}
}
/**
* Apply the current gradient to selected element's fill or stroke.
* @function module:elem-get-set.SvgCanvas#setGradient
* @param {"fill"|"stroke"} type - String indicating "fill" or "stroke" to apply to an element
* @returns {void}
*/
const setGradientMethod = (type) => {
if (!svgCanvas.getCurProperties(`${type}_paint`) ||
svgCanvas.getCurProperties(`${type}_paint`).type === 'solidColor') { return }
const canvas = svgCanvas
let grad = canvas[type + 'Grad']
if (!grad) { return }
// find out if there is a duplicate gradient already in the defs
const duplicateGrad = findDuplicateGradient(grad)
const defs = findDefs()
// no duplicate found, so import gradient into defs
if (!duplicateGrad) {
// const origGrad = grad;
grad = svgCanvas.getDOMDocument().importNode(grad, true)
defs.append(grad)
// get next id and set it on the grad
grad.id = svgCanvas.getNextId()
} else { // use existing gradient
grad = duplicateGrad
}
svgCanvas.setColor(type, `url(#${grad.id})`)
}
/**
* Check if exact gradient already exists.
* @function module:svgcanvas~findDuplicateGradient
* @param {SVGGradientElement} grad - The gradient DOM element to compare to others
* @returns {SVGGradientElement} The existing gradient if found, `null` if not
*/
const findDuplicateGradient = (grad) => {
if (!grad) {
return null
}
if (!['linearGradient', 'radialGradient'].includes(grad.tagName)) {
return null
}
const defs = findDefs()
const existingGrads = defs.querySelectorAll('linearGradient, radialGradient')
let i = existingGrads.length
const radAttrs = ['r', 'cx', 'cy', 'fx', 'fy']
while (i--) {
const og = existingGrads[i]
if (og.tagName !== grad.tagName) {
continue
}
if (grad.tagName === 'linearGradient') {
if (grad.getAttribute('x1') !== og.getAttribute('x1') ||
grad.getAttribute('y1') !== og.getAttribute('y1') ||
grad.getAttribute('x2') !== og.getAttribute('x2') ||
grad.getAttribute('y2') !== og.getAttribute('y2')
) {
continue
}
} else {
const gradAttrs = {
r: Number(grad.getAttribute('r')),
cx: Number(grad.getAttribute('cx')),
cy: Number(grad.getAttribute('cy')),
fx: Number(grad.getAttribute('fx')),
fy: Number(grad.getAttribute('fy'))
}
const ogAttrs = {
r: Number(og.getAttribute('r')),
cx: Number(og.getAttribute('cx')),
cy: Number(og.getAttribute('cy')),
fx: Number(og.getAttribute('fx')),
fy: Number(og.getAttribute('fy'))
}
let diff = false
radAttrs.forEach((attr) => {
if (gradAttrs[attr] !== ogAttrs[attr]) { diff = true }
})
if (diff) { continue }
}
// else could be a duplicate, iterate through stops
const stops = grad.getElementsByTagNameNS(NS.SVG, 'stop')
const ostops = og.getElementsByTagNameNS(NS.SVG, 'stop')
if (stops.length !== ostops.length) {
continue
}
let j = stops.length
while (j--) {
const stop = stops[j]
const ostop = ostops[j]
if (stop.getAttribute('offset') !== ostop.getAttribute('offset') ||
stop.getAttribute('stop-opacity') !== ostop.getAttribute('stop-opacity') ||
stop.getAttribute('stop-color') !== ostop.getAttribute('stop-color')) {
break
}
}
if (j === -1) {
return og
}
} // for each gradient in defs
return null
}
/**
* Set a color/gradient to a fill/stroke.
* @function module:elem-get-set.SvgCanvas#setPaint
* @param {"fill"|"stroke"} type - String with "fill" or "stroke"
* @param {} paint - The paint object to apply
* @returns {void}
*/
const setPaintMethod = (type, paint) => {
// make a copy
const p = new Paint(paint)
svgCanvas.setPaintOpacity(type, p.alpha / 100, true)
// now set the current paint object
svgCanvas.setCurProperties(`${type}_paint`, p)
switch (p.type) {
case 'solidColor':
svgCanvas.setColor(type, p.solidColor !== 'none' ? `#${p.solidColor}` : 'none')
break
case 'linearGradient':
case 'radialGradient':
svgCanvas.setCanvas(type + 'Grad', p[p.type])
svgCanvas.setGradient(type)
break
}
}
/**
* Sets the stroke width for the current selected elements.
* When attempting to set a line's width to 0, this changes it to 1 instead.
* @function module:elem-get-set.SvgCanvas#setStrokeWidth
* @param {Float} val - A Float indicating the new stroke width value
* @fires module:elem-get-set.SvgCanvas#event:changed
* @returns {void}
*/
const setStrokeWidthMethod = (val) => {
const selectedElements = svgCanvas.getSelectedElements()
if (val === 0 && ['line', 'path'].includes(svgCanvas.getMode())) {
svgCanvas.setStrokeWidth(1)
return
}
svgCanvas.setCurProperties('stroke_width', val)
const elems = []
/**
*
* @param {Element} e
* @returns {void}
*/
const addNonG = (e) => {
if (e.nodeName !== 'g') {
elems.push(e)
}
}
let i = selectedElements.length
while (i--) {
const elem = selectedElements[i]
if (elem) {
if (elem.tagName === 'g') {
walkTree(elem, addNonG)
} else {
elems.push(elem)
}
}
}
if (elems.length > 0) {
svgCanvas.changeSelectedAttribute('stroke-width', val, elems)
svgCanvas.call('changed', selectedElements)
}
}
/**
* Set the given stroke-related attribute the given value for selected elements.
* @function module:elem-get-set.SvgCanvas#setStrokeAttr
* @param {string} attr - String with the attribute name
* @param {string|Float} val - String or number with the attribute value
* @fires module:elem-get-set.SvgCanvas#event:changed
* @returns {void}
*/
const setStrokeAttrMethod = (attr, val) => {
const selectedElements = svgCanvas.getSelectedElements()
svgCanvas.setCurShape(attr.replace('-', '_'), val)
const elems = []
let i = selectedElements.length
while (i--) {
const elem = selectedElements[i]
if (elem) {
if (elem.tagName === 'g') {
walkTree(elem, (e) => { if (e.nodeName !== 'g') { elems.push(e) } })
} else {
elems.push(elem)
}
}
}
if (elems.length > 0) {
svgCanvas.changeSelectedAttribute(attr, val, elems)
svgCanvas.call('changed', selectedElements)
}
}
const getSelectedTextElements = () => {
return svgCanvas.getSelectedElements().filter(el => el?.tagName === 'text')
}
const getChangedTextElements = (textElements, attr, newValue) => {
const normalizedValue = String(newValue)
return textElements.filter((elem) => {
const oldValue = attr === '#text' ? elem.textContent : elem.getAttribute(attr)
return (oldValue || '') !== normalizedValue
})
}
const notifyTextChange = (textElements) => {
if (textElements.length > 0) {
svgCanvas.call('changed', textElements)
}
}
/**
* Check if all selected text elements are in bold.
* @function module:svgcanvas.SvgCanvas#getBold
* @returns {boolean} `true` if all selected elements are bold, `false` otherwise.
*/
const getBoldMethod = () => {
const textElements = getSelectedTextElements()
return textElements.every(el => el.getAttribute('font-weight') === 'bold')
}
/**
* Make the selected element(s) bold or normal.
* @function module:svgcanvas.SvgCanvas#setBold
* @param {boolean} b - Indicates bold (`true`) or normal (`false`)
* @returns {void}
*/
const setBoldMethod = (b) => {
const textElements = getSelectedTextElements()
const value = b ? 'bold' : 'normal'
const changedTextElements = getChangedTextElements(textElements, 'font-weight', value)
if (changedTextElements.length > 0) {
svgCanvas.changeSelectedAttribute('font-weight', value, changedTextElements)
}
if (!textElements.some(el => el.textContent)) {
svgCanvas.textActions.setCursor()
}
notifyTextChange(changedTextElements)
}
/**
* Check if all selected text elements have the given text decoration value or not.
* @returns {boolean} Indicates whether or not elements have the text decoration value
*/
const hasTextDecorationMethod = (value) => {
const textElements = getSelectedTextElements()
return textElements.every(el => (el.getAttribute('text-decoration') || '').includes(value))
}
/**
* Adds the given text decoration value
* @param value The text decoration value
* @returns {void}
*/
const addTextDecorationMethod = (value) => {
const { ChangeElementCommand, BatchCommand } = svgCanvas.history
const textElements = getSelectedTextElements()
const batchCmd = new BatchCommand()
textElements.forEach(elem => {
const oldValue = elem.getAttribute('text-decoration') || ''
// Add the new text decoration value if it did not exist
if (!oldValue.includes(value)) {
batchCmd.addSubCommand(new ChangeElementCommand(elem, { 'text-decoration': oldValue }))
svgCanvas.changeSelectedAttributeNoUndo('text-decoration', `${oldValue} ${value}`.trim(), [elem])
}
})
if (!batchCmd.isEmpty()) {
svgCanvas.undoMgr.addCommandToHistory(batchCmd)
}
if (!textElements.some(el => el.textContent)) {
svgCanvas.textActions.setCursor()
}
}
/**
* Removes the given text decoration value
* @param value The text decoration value
* @returns {void}
*/
const removeTextDecorationMethod = (value) => {
const { ChangeElementCommand, BatchCommand } = svgCanvas.history
const textElements = getSelectedTextElements()
const batchCmd = new BatchCommand()
textElements.forEach(elem => {
const actualValues = elem.getAttribute('text-decoration') || ''
batchCmd.addSubCommand(new ChangeElementCommand(elem, { 'text-decoration': actualValues }))
svgCanvas.changeSelectedAttributeNoUndo('text-decoration', actualValues.replace(value, '').trim(), [elem])
})
if (!batchCmd.isEmpty()) {
svgCanvas.undoMgr.addCommandToHistory(batchCmd)
}
if (!textElements.some(el => el.textContent)) {
svgCanvas.textActions.setCursor()
}
}
/**
* Check if all selected elements have an italic font style.
* @function module:svgcanvas.SvgCanvas#getItalic
* @returns {boolean} `true` if all selected elements are in italics, `false` otherwise.
*/
const getItalicMethod = () => {
const textElements = getSelectedTextElements()
return textElements.every(el => el.getAttribute('font-style') === 'italic')
}
/**
* Make the selected element(s) italic or normal.
* @function module:svgcanvas.SvgCanvas#setItalic
* @param {boolean} i - Indicates italic (`true`) or normal (`false`)
* @returns {void}
*/
const setItalicMethod = (i) => {
const textElements = getSelectedTextElements()
const value = i ? 'italic' : 'normal'
const changedTextElements = getChangedTextElements(textElements, 'font-style', value)
if (changedTextElements.length > 0) {
svgCanvas.changeSelectedAttribute('font-style', value, changedTextElements)
}
if (!textElements.some(el => el.textContent)) {
svgCanvas.textActions.setCursor()
}
notifyTextChange(changedTextElements)
}
/**
* @function module:svgcanvas.SvgCanvas#setTextAnchorMethod Set the new text anchor
* @param {string} value - The text anchor value (start, middle or end)
* @returns {void}
*/
const setTextAnchorMethod = (value) => {
const textElements = getSelectedTextElements()
const changedTextElements = getChangedTextElements(textElements, 'text-anchor', value)
if (changedTextElements.length > 0) {
svgCanvas.changeSelectedAttribute('text-anchor', value, changedTextElements)
}
notifyTextChange(changedTextElements)
}
/**
* @function module:svgcanvas.SvgCanvas#setLetterSpacingMethod Set the new letter spacing
* @param {string} value - The letter spacing value
* @returns {void}
*/
const setLetterSpacingMethod = (value) => {
const textElements = getSelectedTextElements()
const changedTextElements = getChangedTextElements(textElements, 'letter-spacing', value)
if (changedTextElements.length > 0) {
svgCanvas.changeSelectedAttribute('letter-spacing', value, changedTextElements)
}
if (!textElements.some(el => el.textContent)) {
svgCanvas.textActions.setCursor()
}
notifyTextChange(changedTextElements)
}
/**
* @function module:svgcanvas.SvgCanvas#setWordSpacingMethod Set the new word spacing
* @param {string} value - The word spacing value
* @returns {void}
*/
const setWordSpacingMethod = (value) => {
const textElements = getSelectedTextElements()
const changedTextElements = getChangedTextElements(textElements, 'word-spacing', value)
if (changedTextElements.length > 0) {
svgCanvas.changeSelectedAttribute('word-spacing', value, changedTextElements)
}
if (!textElements.some(el => el.textContent)) {
svgCanvas.textActions.setCursor()
}
notifyTextChange(changedTextElements)
}
/**
* @function module:svgcanvas.SvgCanvas#setTextLengthMethod Set the new text length
* @param {string} value - The text length value
* @returns {void}
*/
const setTextLengthMethod = (value) => {
const textElements = getSelectedTextElements()
const changedTextElements = getChangedTextElements(textElements, 'textLength', value)
if (changedTextElements.length > 0) {
svgCanvas.changeSelectedAttribute('textLength', value, changedTextElements)
}
if (!textElements.some(el => el.textContent)) {
svgCanvas.textActions.setCursor()
}
notifyTextChange(changedTextElements)
}
/**
* @function module:svgcanvas.SvgCanvas#setLengthAdjustMethod Set the new length adjust
* @param {string} value - The length adjust value
* @returns {void}
*/
const setLengthAdjustMethod = (value) => {
const textElements = getSelectedTextElements()
const changedTextElements = getChangedTextElements(textElements, 'lengthAdjust', value)
if (changedTextElements.length > 0) {
svgCanvas.changeSelectedAttribute('lengthAdjust', value, changedTextElements)
}
if (!textElements.some(el => el.textContent)) {
svgCanvas.textActions.setCursor()
}
notifyTextChange(changedTextElements)
}
/**
* @function module:svgcanvas.SvgCanvas#getFontFamily
* @returns {string} The current font family
*/
const getFontFamilyMethod = () => {
return svgCanvas.getCurText('font_family')
}
/**
* Set the new font family.
* @function module:svgcanvas.SvgCanvas#setFontFamily
* @param {string} val - String with the new font family
* @returns {void}
*/
const setFontFamilyMethod = (val) => {
const textElements = getSelectedTextElements()
const changedTextElements = getChangedTextElements(textElements, 'font-family', val)
svgCanvas.setCurText('font_family', val)
if (changedTextElements.length > 0) {
svgCanvas.changeSelectedAttribute('font-family', val, changedTextElements)
}
if (!textElements.some(el => el.textContent)) {
svgCanvas.textActions.setCursor()
}
notifyTextChange(changedTextElements)
}
/**
* Set the new font color.
* @function module:svgcanvas.SvgCanvas#setFontColor
* @param {string} val - String with the new font color
* @returns {void}
*/
const setFontColorMethod = (val) => {
const textElements = getSelectedTextElements()
const changedTextElements = getChangedTextElements(textElements, 'fill', val)
svgCanvas.setCurText('fill', val)
if (changedTextElements.length > 0) {
svgCanvas.changeSelectedAttribute('fill', val, changedTextElements)
}
notifyTextChange(changedTextElements)
}
/**
* @function module:svgcanvas.SvgCanvas#getFontColor
* @returns {string} The current font color
*/
const getFontColorMethod = () => {
return svgCanvas.getCurText('fill')
}
/**
* @function module:svgcanvas.SvgCanvas#getFontSize
* @returns {Float} The current font size
*/
const getFontSizeMethod = () => {
return svgCanvas.getCurText('font_size')
}
/**
* Applies the given font size to the selected element.
* @function module:svgcanvas.SvgCanvas#setFontSize
* @param {Float} val - Float with the new font size
* @returns {void}
*/
const setFontSizeMethod = (val) => {
const textElements = getSelectedTextElements()
const changedTextElements = getChangedTextElements(textElements, 'font-size', val)
svgCanvas.setCurText('font_size', val)
if (changedTextElements.length > 0) {
svgCanvas.changeSelectedAttribute('font-size', val, changedTextElements)
}
if (!textElements.some(el => el.textContent)) {
svgCanvas.textActions.setCursor()
}
notifyTextChange(changedTextElements)
}
/**
* @function module:svgcanvas.SvgCanvas#getText
* @returns {string} The current text (`textContent`) of the selected element
*/
const getTextMethod = () => {
const selectedElements = svgCanvas.getSelectedElements()
const selected = selectedElements[0]
return (selected) ? selected.textContent : ''
}
/**
* Updates the text element with the given string.
* @function module:svgcanvas.SvgCanvas#setTextContent
* @param {string} val - String with the new text
* @returns {void}
*/
const setTextContentMethod = (val) => {
svgCanvas.changeSelectedAttribute('#text', val)
svgCanvas.textActions.init(val)
svgCanvas.textActions.setCursor()
}
/**
* Sets the new image URL for the selected image element. Updates its size if
* a new URL is given.
* @function module:svgcanvas.SvgCanvas#setImageURL
* @param {string} val - String with the image URL/path
* @fires module:svgcanvas.SvgCanvas#event:changed
* @returns {void}
*/
const setImageURLMethod = (val) => {
const { ChangeElementCommand, BatchCommand } = svgCanvas.history
const selectedElements = svgCanvas.getSelectedElements()
const elem = selectedElements[0]
if (!elem) { return }
const attrs = {
width: elem.getAttribute('width'),
height: elem.getAttribute('height')
}
const setsize = (!attrs.width || !attrs.height)
const curHref = getHref(elem)
const hrefChanged = curHref !== val
// Do nothing if no URL change or size change
if (!hrefChanged && !setsize) {
return
}
const batchCmd = new BatchCommand('Change Image URL')
if (hrefChanged) {
setHref(elem, val)
batchCmd.addSubCommand(new ChangeElementCommand(elem, {
'#href': curHref
}))
}
let finalized = false
const finalize = () => {
if (finalized) { return }
finalized = true
if (batchCmd.isEmpty()) { return }
svgCanvas.addCommandToHistory(batchCmd)
svgCanvas.call('changed', [elem])
}
const img = new Image()
img.onload = () => {
const changes = {
width: elem.getAttribute('width'),
height: elem.getAttribute('height')
}
elem.setAttribute('width', img.width)
elem.setAttribute('height', img.height)
const selector = svgCanvas.selectorManager.requestSelector(elem)
selector && selector.resize()
batchCmd.addSubCommand(new ChangeElementCommand(elem, changes))
finalize()
}
img.onerror = () => {
finalize()
}
img.src = val
}
/**
* Sets the new link URL for the selected anchor element.
* @function module:svgcanvas.SvgCanvas#setLinkURL
* @param {string} val - String with the link URL/path
* @returns {void}
*/
const setLinkURLMethod = (val) => {
const { ChangeElementCommand, BatchCommand } = svgCanvas.history
const selectedElements = svgCanvas.getSelectedElements()
let elem = selectedElements[0]
if (!elem) { return }
if (elem.tagName !== 'a') {
// See if parent is an anchor
const parentsA = getParents(elem.parentNode, 'a')
if (parentsA?.length) {
elem = parentsA[0]
} else {
return
}
}
const curHref = getHref(elem)
if (curHref === val) { return }
const batchCmd = new BatchCommand('Change Link URL')
setHref(elem, val)
batchCmd.addSubCommand(new ChangeElementCommand(elem, {
'#href': curHref
}))
svgCanvas.addCommandToHistory(batchCmd)
}
/**
* Sets the `rx` and `ry` values to the selected `rect` element
* to change its corner radius.
* @function module:svgcanvas.SvgCanvas#setRectRadius
* @param {string|Float} val - The new radius
* @fires module:svgcanvas.SvgCanvas#event:changed
* @returns {void}
*/
const setRectRadiusMethod = (val) => {
const { ChangeElementCommand } = svgCanvas.history
const selectedElements = svgCanvas.getSelectedElements()
const selected = selectedElements[0]
if (selected?.tagName !== 'rect') { return }
const radius = Number(val)
if (!Number.isFinite(radius) || radius < 0) {
return
}
const oldRx = selected.getAttribute('rx')
const oldRy = selected.getAttribute('ry')
const currentRx = Number(oldRx)
const currentRy = Number(oldRy)
const hasCurrentRx = oldRx !== null && Number.isFinite(currentRx)
const hasCurrentRy = oldRy !== null && Number.isFinite(currentRy)
const already = (radius === 0 && oldRx === null && oldRy === null) ||
(hasCurrentRx && hasCurrentRy && currentRx === radius && currentRy === radius)
if (already) { return }
selected.setAttribute('rx', radius)
selected.setAttribute('ry', radius)
svgCanvas.addCommandToHistory(new ChangeElementCommand(selected, { rx: oldRx, ry: oldRy }, 'Radius'))
svgCanvas.call('changed', [selected])
}
/**
* Wraps the selected element(s) in an anchor element or converts group to one.
* @function module:svgcanvas.SvgCanvas#makeHyperlink
* @param {string} url
* @returns {void}
*/
const makeHyperlinkMethod = (url) => {
svgCanvas.groupSelectedElements('a', url)
}
/**
* @function module:svgcanvas.SvgCanvas#removeHyperlink
* @returns {void}
*/
const removeHyperlinkMethod = () => {
svgCanvas.ungroupSelectedElement()
}
/**
* Group: Element manipulation.
*/
/**
* Sets the new segment type to the selected segment(s).
* @function module:svgcanvas.SvgCanvas#setSegType
* @param {Integer} newType - New segment type. See {@link https://www.w3.org/TR/SVG/paths.html#InterfaceSVGPathSeg} for list
* @returns {void}
*/
const setSegTypeMethod = (newType) => {
svgCanvas.pathActions.setSegType(newType)
}
/**
* Set the background of the editor (NOT the actual document).
* @function module:svgcanvas.SvgCanvas#setBackground
* @param {string} color - String with fill color to apply
* @param {string} url - URL or path to image to use
* @returns {void}
*/
const setBackgroundMethod = (color, url) => {
const bg = getElement('canvasBackground')
if (!bg) { return }
const border = bg.querySelector('rect')
if (!border) { return }
let bgImg = getElement('background_image')
let bgPattern = getElement('background_pattern')
border.setAttribute('fill', color === 'chessboard' ? '#fff' : color)
if (color === 'chessboard') {
if (!bgPattern) {
bgPattern = svgCanvas.getDOMDocument().createElementNS(NS.SVG, 'foreignObject')
svgCanvas.assignAttributes(bgPattern, {
id: 'background_pattern',
width: '100%',
height: '100%',
preserveAspectRatio: 'xMinYMin',
style: 'pointer-events:none'
})
const div = document.createElement('div')
svgCanvas.assignAttributes(div, {
style: 'pointer-events:none;width:100%;height:100%;' +
'background-image:url(data:image/gif;base64,' +
'R0lGODlhEAAQAIAAAP///9bW1iH5BAAAAAAALAAAAAAQABAAAAIfjG+' +
'gq4jM3IFLJgpswNly/XkcBpIiVaInlLJr9FZWAQA7);'
})
bgPattern.append(div)
bg.append(bgPattern)
}
} else if (bgPattern) {
bgPattern.remove()
}
if (url) {
if (!bgImg) {
bgImg = svgCanvas.getDOMDocument().createElementNS(NS.SVG, 'image')
svgCanvas.assignAttributes(bgImg, {
id: 'background_image',
width: '100%',
height: '100%',
preserveAspectRatio: 'xMinYMin',
style: 'pointer-events:none'
})
}
setHref(bgImg, url)
bg.append(bgImg)
} else if (bgImg) {
bgImg.remove()
}
}