Skip to content

Commit

Permalink
fix: scope the svgStyle to prevent style bleeding
Browse files Browse the repository at this point in the history
  • Loading branch information
robertrosman committed Jun 3, 2024
1 parent bdea68f commit 9a86367
Show file tree
Hide file tree
Showing 6 changed files with 103 additions and 74 deletions.
18 changes: 16 additions & 2 deletions src/components/VpImage.vue
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import { computed, ref, toRefs, unref } from 'vue'
import type { Shape, Tool, ToolType } from '../types'
import { useSimplifiedHistory } from '@/composables/useSimplifiedHistory';
import { randomId } from '@/utils/randomId';
const props = defineProps<{
tools: Tool<any>[]
Expand All @@ -12,6 +13,7 @@ const props = defineProps<{
}>()
const svg = ref()
const svgId = ref(randomId())
defineExpose({
svg
Expand All @@ -21,7 +23,19 @@ function getTool(toolType: ToolType) {
return props.tools.find((tool) => tool.type === toolType)
}
const style = computed(() => props.tools.map((tool) => tool.svgStyle ? unref(tool.svgStyle) : '').join('\n'))
const style = computed(() => {
return props.tools.map((tool) => {
if (!tool.svgStyle) {
return ''
}
else if (typeof tool.svgStyle === 'function') {
return tool.svgStyle({ svgId: svgId.value })
}
else {
return unref(tool.svgStyle)
}
}).join('\n')
})
const { simplifiedHistory } = useSimplifiedHistory({ ...toRefs(props), includeActiveShape: false })
Expand All @@ -38,7 +52,7 @@ const highLayers = computed(() =>
</script>

<template>
<svg ref="svg" :viewBox="`0 0 ${width} ${height}`" xmlns="http://www.w3.org/2000/svg" class="vp-image">
<svg ref="svg" :viewBox="`0 0 ${width} ${height}`" :id="svgId" xmlns="http://www.w3.org/2000/svg" class="vp-image">

<component v-for="tool in lowLayers" :key="tool.type" :is="tool.ToolSvgComponent" :history :tools :activeShape
:width :height />
Expand Down
46 changes: 24 additions & 22 deletions src/composables/tools/useMove/useMove.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { useSimplifiedHistory } from '@/composables/useSimplifiedHistory'
import type { BaseShape, DrawEvent, ExportParameters, ImageHistory, Movement, Shape, Tool, ToolSvgComponentProps } from '@/types'
import type { BaseShape, DrawEvent, ExportParameters, ImageHistory, Movement, Shape, SvgStyleParameters, Tool, ToolSvgComponentProps } from '@/types'
import { h, toRefs } from 'vue'

export interface Move extends BaseShape, Movement {
Expand Down Expand Up @@ -97,27 +97,29 @@ export function useMove({
layer: 1_000
}

const svgStyle = `
circle.handle {
r: 0;
stroke: #000;
stroke-width: 2;
stroke-opacity: 0.5;
fill: #fff;
fill-opacity: 0.3;
transition: r 0.1s ease-out;
}
.active-tool-move circle.handle,
.vp-editor circle.handle.is-active {
r: ${handleRadius};
}
.active-tool-move circle.handle:hover,
.vp-editor circle.handle.is-active:hover {
r: ${handleRadius * 1.5};
}
`
function svgStyle ({ svgId }: SvgStyleParameters) {
return `
circle.handle {
r: 0;
stroke: #000;
stroke-width: 2;
stroke-opacity: 0.5;
fill: #fff;
fill-opacity: 0.3;
transition: r 0.1s ease-out;
}
.active-tool-move #${svgId} circle.handle,
.vp-editor #${svgId} circle.handle.is-active {
r: ${handleRadius};
}
.active-tool-move #${svgId} circle.handle:hover,
.vp-editor #${svgId} circle.handle.is-active:hover {
r: ${handleRadius * 1.5};
}
`
}

function beforeExport({ svg }: ExportParameters) {
svg.querySelectorAll('circle.handle').forEach(handle => handle.remove())
Expand Down
87 changes: 44 additions & 43 deletions src/composables/tools/useTextarea/useTextarea.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { createDataUrl, urlToBlob } from '@/main'
import type { BaseShape, DrawEvent, ExportParameters, Movement, Tool } from '@/types'
import type { BaseShape, DrawEvent, ExportParameters, Movement, SvgStyleParameters, Tool } from '@/types'
import { rectangleHandles } from '@/composables/tools/useMove/handles/rectangleHandles'
import { createShapeSvgComponent } from '@/utils/createShapeSvgComponent'
import { computed, h, ref } from 'vue'
Expand Down Expand Up @@ -118,56 +118,57 @@ export function useTextarea({
}))
)

const svgStyle = computed(() =>
(customFont.value
? `
@font-face {
font-family: "${font}";
font-style: normal;
font-weight: 400;
font-display: swap;
src: url(${customFont.value}) format("woff2");
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
`
: ''
)

+ `
.vp-image {
font-size: ${baseFontSize}px;
user-select: none;
}
function svgStyle ({ svgId }: SvgStyleParameters) {
return (customFont.value
? `
@font-face {
font-family: "${font}";
font-style: normal;
font-weight: 400;
font-display: swap;
src: url(${customFont.value}) format("woff2");
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
`
: ''
)

+ `
#${svgId} {
font-size: ${baseFontSize}px;
user-select: none;
}
.textarea {
width: 100%;
height: 100%;
border: none;
outline: none;
background: transparent;
resize: none;
touch-action: none;
overflow: hidden;
font-family: "${font}", Arial, sans-serif;
padding: 1px;
cursor: inherit;
}
#${svgId} .textarea {
width: 100%;
height: 100%;
border: none;
outline: none;
background: transparent;
resize: none;
touch-action: none;
overflow: hidden;
font-family: "${font}", Arial, sans-serif;
padding: 1px;
cursor: inherit;
}
.textarea.is-active {
cursor: text;
}
#${svgId} .textarea.is-active {
cursor: text;
}
.textarea.is-active, .active-tool-move .textarea {
border: 1px dashed #777;
padding: 0;
}
`)
#${svgId} .textarea.is-active, .active-tool-move #${svgId} .textarea {
border: 1px dashed #777;
padding: 0;
}
`
}

function beforeExport({ svg, history }: ExportParameters) {
// Remove styles if no textarea shape exist, to shrink image size
const styleElement = svg.querySelector('style')
if (!history.some(shape => shape.type === 'textarea') && styleElement) {
styleElement.innerHTML = styleElement.innerHTML.replace(svgStyle.value, '')
styleElement.innerHTML = styleElement.innerHTML.replace(svgStyle({ svgId: svg.id }), '')
}
}

Expand Down
6 changes: 5 additions & 1 deletion src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,10 @@ export interface ToolSvgComponentProps extends SvgComponentProps {
activeShape?: Shape
}

export interface SvgStyleParameters {
svgId: string
}

/** Every new shape must extend this base class. */
export interface BaseShape {
/** What type of shape is produced? Must be unique between tools. */
Expand Down Expand Up @@ -102,7 +106,7 @@ export interface Tool<T extends BaseShape> {
* Here you can add styling that can apply to your svg element. Please scope it in classes, like `.your-tool { opacity: 0.5 }` so
* it doesn't affect other elements in the same svg.
*/
svgStyle?: MaybeRef<string>
svgStyle?: MaybeRef<string> | ((args: SvgStyleParameters) => string)

/**
* This is pretty much the same concept as shapeSvg, except it is only rendered once per image. For some tools it might be sufficient
Expand Down
11 changes: 8 additions & 3 deletions src/utils/randomId.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,13 @@ describe('randomId', () => {
expect(typeof id).toBe('string')
})

it('should return a 7 letter string padded with zeros', () => {
const id = randomId(100)
expect(id).toBe('000002s')
it('should return the lowest possible id (beginning with a letter)', () => {
const id = randomId(() => 0)
expect(id).toBe('a000000')
})

it('should return the lowest possible id (beginning with a letter)', () => {
const id = randomId(() => 0.9999999999)
expect(id).toBe('zzzzzzz')
})
})
9 changes: 6 additions & 3 deletions src/utils/randomId.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
/** Simple function to generate a random id. It is "unique" in the sense "the chance for collision is small enough", but not universally unique. */
export function randomId(seed: number = Math.random() * 36 * 36 * 36 * 36 * 36 * 36 * 36) {
return Math.round(seed).toString(36).padStart(7, '0')
/**
* Simple function to generate a random id. It is "unique" in the sense "the chance for collision is small enough", but not universally unique.
* The generated id will start with a letter in order to be a valid css selector.
*/
export function randomId(random = Math.random) {
return [26, 36, 36, 36, 36, 36, 36].map(x => Math.floor(random() * x + (36 - x)).toString(36)).join('')
}

0 comments on commit 9a86367

Please sign in to comment.