]> source.dussan.org Git - svg.js.git/commitdiff
added Fragment, completed Dom Tests, fixed `matches()` for document fragments
authorUlrich-Matthias Schäfer <ulima.ums@googlemail.com>
Tue, 7 Apr 2020 21:32:38 +0000 (07:32 +1000)
committerUlrich-Matthias Schäfer <ulima.ums@googlemail.com>
Tue, 7 Apr 2020 21:32:38 +0000 (07:32 +1000)
CHANGELOG.md
spec/spec/elements/Dom.js
spec/spec/elements/Fragment.js [new file with mode: 0644]
spec/spec/utils/adopter.js
src/elements/Dom.js
src/elements/Fragment.js [new file with mode: 0644]
src/main.js
src/utils/adopter.js
svg.js.d.ts

index 1152156765308efdea26f10af5b55cd18ad1f641..83af8ea6c8860cd4ab0664e97469e4b7350f3e4e 100644 (file)
@@ -19,9 +19,10 @@ The document follows the conventions described in [“Keep a CHANGELOG”](http:
  - fixed tons of typings in the svg.d.ts file and relaxed type requirements for `put()` and `parent()`
  - fixed adopter when adopting an svg/html string. It had still its wrapper as parentNode attached
  - fixed `put()` which correctly creates an svgjs object from the passed element now before returning
- - fixed `parent()` which correctly returns null if direct parent is the document or a document-fragment
+ - fixed `parent()` which correctly returns a Dom instance when parent is the document or document-fragment
  - fixed `add()` which correctly removes namespaces of non-root svg elements now when added to another svg element (#1086)
  - fixed `isRoot()` which correctly returns false, if the element is in a document-fragment
+ - fixed `replace()` which works without a parent now, too
 
 ### Added
  - added second Parameter to `SVG(el, isHTML)` which allows to explicitely create elements in the HTML namespace (#1058)
@@ -31,6 +32,7 @@ The document follows the conventions described in [“Keep a CHANGELOG”](http:
  - added `options` parameter to `dispatch()` and `fire()` to allow for more special needs
  - added `newLine()` constructor to `Text` to create a tspan marked as new line (#1088)
  - added lots of tests in es6 format
+ - added `Fragment` as a wrapper for document-fragment
 
 ## [3.0.16] - 2019-11-12
 
index a1e2a6d08305b16bea106f7adf6dc6142b3e5a8f..6689822a2200782df63ce1f287275cadf8cf171e 100644 (file)
@@ -1,6 +1,6 @@
 /* globals describe, expect, it, beforeEach, spyOn, jasmine, container */
 
-import { SVG, G, Rect, Svg, Dom, List } from '../../../src/main.js'
+import { SVG, G, Rect, Svg, Dom, List, Fragment, Circle, Tspan } from '../../../src/main.js'
 import { getWindow } from '../../../src/utils/window.js'
 const { any, createSpy, objectContaining } = jasmine
 
@@ -63,6 +63,16 @@ describe('Dom.js', function () {
       expect(g.children().length).toBe(1)
       expect(g.get(0)).toBe(rect)
     })
+
+    it('handles a node', () => {
+      const g = new G()
+      const rect = new Rect()
+      const node = rect.node
+      delete rect.instance
+      g.add(node)
+      expect(g.children().length).toBe(1)
+      expect(g.get(0)).toEqual(any(Rect))
+    })
   })
 
   describe('addTo()', () => {
@@ -249,6 +259,47 @@ describe('Dom.js', function () {
     })
   })
 
+  describe('id()', () => {
+    it('returns current element when called as setter', () => {
+      const g = new G()
+      expect(g.id('asd')).toBe(g)
+    })
+
+    it('sets the id with argument given', () => {
+      expect(new G().id('foo').node.id).toBe('foo')
+    })
+
+    it('gets the id when no argument given', () => {
+      const g = new G({ id: 'foo' })
+      expect(g.id()).toBe('foo')
+    })
+
+    it('generates an id on getting if none is set', () => {
+      const g = new G()
+      expect(g.node.id).toBe('')
+      g.id()
+      expect(g.node.id).not.toBe('')
+    })
+  })
+
+  describe('index()', () => {
+    it('gets the position of the passed child', () => {
+      const g = new G()
+      g.rect(100, 100)
+      const rect = g.rect(100, 100)
+      expect(g.index(rect)).toBe(1)
+    })
+  })
+
+  describe('last()', () => {
+    it('gets the last child of the element', () => {
+      const g = new G()
+      g.rect(100, 100)
+      const rect = g.rect(100, 100)
+      expect(g.last()).toBe(rect)
+    })
+  })
+
   describe('parent()', () => {
     var canvas, rect, group1, group2
 
@@ -282,14 +333,10 @@ describe('Dom.js', function () {
       expect(rect.parent('.not-there')).toBe(null)
     })
 
-    it('returns null if parent is #document', () => {
-      // cant test that here
-    })
-
-    it('returns null if parent is #document-fragment', () => {
+    it('returns Dom if parent is #document-fragment', () => {
       const fragment = getWindow().document.createDocumentFragment()
       const svg = new Svg().addTo(fragment)
-      expect(svg.parent()).toBe(null)
+      expect(svg.parent()).toEqual(any(Dom))
     })
 
     it('returns html parents, too', () => {
@@ -297,6 +344,263 @@ describe('Dom.js', function () {
     })
   })
 
+  describe('put()', () => {
+    it('calls add() but returns the added element instead', () => {
+      const g = new G()
+      const rect = new Rect()
+      const spy = spyOn(g, 'add').and.callThrough()
+      expect(g.put(rect, 0)).toBe(rect)
+      expect(spy).toHaveBeenCalledWith(rect, 0)
+    })
+
+    it('creates object from svg string', () => {
+      const g = new G()
+      const rect = '<rect>'
+      const spy = spyOn(g, 'add').and.callThrough()
+      const ret = g.put(rect, 0)
+      expect(ret).toEqual(any(Rect))
+      expect(spy).toHaveBeenCalledWith(ret, 0)
+    })
+
+    it('works with a query selector', () => {
+      const canvas = SVG().addTo(container)
+      const rect = canvas.rect().addClass('test')
+      const g = canvas.group()
+      const spy = spyOn(g, 'add').and.callThrough()
+      const ret = g.put('.test', 0)
+      expect(ret).toEqual(rect)
+      expect(spy).toHaveBeenCalledWith(rect, 0)
+    })
+  })
+
+  describe('putIn()', () => {
+    it('calls add on the given parent', () => {
+      const g = new G()
+      const rect = new Rect()
+      const spy = spyOn(g, 'add')
+      rect.putIn(g, 0)
+      expect(spy).toHaveBeenCalledWith(rect, 0)
+    })
+
+    it('returns the passed element', () => {
+      const g = new G()
+      const rect = new Rect()
+      expect(rect.putIn(g, 0)).toBe(g)
+    })
+
+    it('returns an instance when svg string given', () => {
+      const g = '<g>'
+      const rect = new Rect()
+      const ret = rect.putIn(g)
+      expect(ret).toEqual(any(G))
+      expect(ret.children()).toEqual([ rect ])
+    })
+
+    it('works with a query selector', () => {
+      const canvas = SVG().addTo(container)
+      const g = canvas.group().addClass('test')
+      const rect = canvas.rect(100, 100)
+      const ret = rect.putIn('.test')
+      expect(ret).toBe(g)
+      expect(g.children()).toEqual([ rect ])
+    })
+  })
+
+  describe('remove()', () => {
+    it('returns the removed element', () => {
+      const canvas = SVG().addTo(container)
+      const rect = canvas.rect(100, 100)
+      expect(rect.remove()).toBe(rect)
+    })
+
+    it('removes the element from the parent', () => {
+      const canvas = SVG().addTo(container)
+      const rect = canvas.rect(100, 100)
+      expect(canvas.children()).toEqual([ rect ])
+      rect.remove()
+      expect(canvas.children()).toEqual([])
+    })
+
+    it('is a noop when element is not attached to the dom', () => {
+      const rect = new Rect()
+      expect(rect.remove()).toBe(rect)
+    })
+
+    it('also works when direct child of document-fragment', () => {
+      const fragment = new Fragment()
+      const rect = fragment.rect(100, 100)
+      expect(fragment.children()).toEqual([ rect ])
+      expect(rect.remove()).toBe(rect)
+      expect(fragment.children()).toEqual([])
+    })
+  })
+
+  describe('removeElement()', () => {
+    it('returns itself', () => {
+      const g = new G()
+      const rect = g.rect(100, 100)
+      expect(g.removeElement(rect)).toBe(g)
+    })
+
+    it('removes the given child', () => {
+      const g = new G()
+      const rect = g.rect(100, 100)
+      expect(g.removeElement(rect).children()).toEqual([])
+    })
+
+    it('throws if the given element is not a child', () => {
+      const g = new G()
+      const rect = new Rect()
+      try {
+        g.removeElement(rect)
+      } catch (e) {
+        expect(e).toEqual(objectContaining({ code: 8 }))
+      }
+    })
+  })
+
+  describe('replace()', () => {
+    it('returns the new element', () => {
+      const g = new G()
+      const rect = g.rect(100, 100)
+      const circle = new Circle()
+      expect(rect.replace(circle)).toBe(circle)
+    })
+
+    it('replaces the child at the correct position', () => {
+      const g = new G()
+      const rect1 = g.rect(100, 100)
+      const rect2 = g.rect(100, 100)
+      const rect3 = g.rect(100, 100)
+      const circle = new Circle()
+      rect2.replace(circle)
+      expect(g.children()).toEqual([ rect1, circle, rect3 ])
+    })
+
+    it('also works without a parent', () => {
+      const rect = new Rect()
+      const circle = new Circle()
+      expect(rect.replace(circle)).toBe(circle)
+    })
+  })
+
+  describe('round()', () => {
+    it('rounds all attributes whose values are numbers to two decimals by default', () => {
+      const rect = new Rect({ id: 'foo', x: 10.678, y: 3, width: 123.456 })
+      expect(rect.round().attr()).toEqual({ id: 'foo', x: 10.68, y: 3, width: 123.46 })
+    })
+
+    it('rounds all attributes whose values are numbers to the passed precision', () => {
+      const rect = new Rect({ id: 'foo', x: 10.678, y: 3, width: 123.456 })
+      expect(rect.round(1).attr()).toEqual({ id: 'foo', x: 10.7, y: 3, width: 123.5 })
+    })
+
+    it('rounds the given attribues whose values are numbers to the passed precision', () => {
+      const rect = new Rect({ id: 'foo', x: 10.678, y: 3, width: 123.456 })
+      expect(rect.round(1, [ 'id', 'x' ]).attr()).toEqual({ id: 'foo', x: 10.7, y: 3, width: 123.456 })
+    })
+  })
+
+  describe('svg()', () => {
+    describe('as setter', () => {
+      it('returns itself', () => {
+        const g = new G()
+        expect(g.svg('<rect>')).toBe(g)
+      })
+
+      it('imports a single element', () => {
+        const g = new G().svg('<rect>')
+        expect(g.children()).toEqual([ any(Rect) ])
+      })
+
+      it('imports multiple elements', () => {
+        const g = new G().svg('<rect /><circle />')
+        expect(g.children()).toEqual([ any(Rect), any(Circle) ])
+      })
+
+      it('replaces the current element with the imported elements with outerHtml = true', () => {
+        const canvas = new Svg()
+        const g = canvas.group()
+        g.svg('<rect /><circle />', true)
+        expect(canvas.children()).toEqual([ any(Rect), any(Circle) ])
+      })
+
+      it('returns the parent when outerHtml = true', () => {
+        const canvas = new Svg()
+        const g = canvas.group()
+        expect(g.svg('<rect><circle>', true)).toBe(canvas)
+      })
+    })
+
+    describe('as getter', () => {
+      let canvas, group, rect
+
+      beforeEach(() => {
+        canvas = new Svg().removeNamespace()
+        group = canvas.group()
+        rect = group.rect(123.456, 234.567)
+      })
+
+      it('returns the svg string of the element by default', () => {
+        expect(rect.svg()).toBe('<rect width="123.456" height="234.567"></rect>')
+        expect(canvas.svg()).toBe('<svg><g><rect width="123.456" height="234.567"></rect></g></svg>')
+      })
+
+      it('returns the innerHtml when outerHtml = false', () => {
+        expect(rect.svg(false)).toBe('')
+        expect(canvas.svg(false)).toBe('<g><rect width="123.456" height="234.567"></rect></g>')
+      })
+
+      it('runs a function on every exported node', () => {
+        expect(rect.svg((el) => el.round(1))).toBe('<rect width="123.5" height="234.6"></rect>')
+      })
+
+      it('runs a function on every exported node and replaces node with returned node if return value is not falsy', () => {
+        expect(rect.svg((el) => new Circle())).toBe('<circle></circle>')
+        expect(canvas.svg((el) => new G())).toBe('<g></g>') // outer <svg> was replaced by an empty g
+        expect(canvas.svg((el) => {
+          if (el instanceof Rect) return new Circle()
+          if (el instanceof Svg) el.removeNamespace()
+        })).toBe('<svg><g><circle></circle></g></svg>')
+      })
+
+      it('runs a function on every exported node and removes node if return value is false', () => {
+        expect(group.svg(() => false)).toBe('')
+        expect(canvas.svg(() => false)).toBe('')
+        expect(canvas.svg((el) => {
+          if (el instanceof Svg) {
+            el.removeNamespace()
+          } else {
+            return false
+          }
+        })).toBe('<svg></svg>')
+      })
+
+      it('runs a function on every inner node and exports it when outerHtml = false', () => {
+        expect(canvas.svg(() => false), false).toBe('')
+        expect(canvas.svg(() => undefined, false)).toBe('<g><rect width="123.456" height="234.567"></rect></g>')
+      })
+
+    })
+
+  })
+
+  describe('toString()', () => {
+    it('calls id() and returns its result', () => {
+      const rect = new Rect({ id: 'foo' })
+      const spy = spyOn(rect, 'id').and.callThrough()
+      expect(rect.toString()).toBe('foo')
+      expect(spy).toHaveBeenCalled()
+    })
+  })
+
+  describe('words', () => {
+    it('sets the nodes textContent to the given value', () => {
+      const tspan = new Tspan().words('Hello World')
+      expect(tspan.text()).toBe('Hello World')
+    })
+  })
+
   describe('wrap()', function () {
     var canvas
     var rect
@@ -361,4 +665,8 @@ describe('Dom.js', function () {
       expect(rect.parent().parent()).toBe(canvas)
     })
   })
+
+  describe('writeDataToDom()', () => {
+    // not really testable
+  })
 })
diff --git a/spec/spec/elements/Fragment.js b/spec/spec/elements/Fragment.js
new file mode 100644 (file)
index 0000000..c0f5f0f
--- /dev/null
@@ -0,0 +1,61 @@
+/* globals describe, expect, it, spyOn, jasmine */
+
+import { Fragment, Dom } from '../../../src/main.js'
+import { getWindow } from '../../../src/utils/window.js'
+
+const { any } = jasmine
+
+describe('Fragment.js', () => {
+
+  describe('()', () => {
+    it('creates a new object of type Fragment', () => {
+      expect(new Fragment()).toEqual(any(Fragment))
+    })
+
+    it('uses passed node instead of creating', () => {
+      const fragment = getWindow().document.createDocumentFragment()
+      expect(new Fragment(fragment).node).toBe(fragment)
+    })
+
+    it('has all Container methods available', () => {
+      const frag = new Fragment()
+      const rect = frag.rect(100, 100)
+
+      expect(frag.children()).toEqual([ rect ])
+    })
+  })
+
+  describe('svg()', () => {
+    describe('as setter', () => {
+      it('calls parent method with outerHtml = false', () => {
+        const frag = new Fragment()
+        const spy = spyOn(Dom.prototype, 'svg').and.callThrough()
+        frag.svg('<rect>', true)
+        expect(spy).toHaveBeenCalledWith('<rect>', false)
+      })
+    })
+
+    describe('as getter', () => {
+      it('calls parent method with outerHtml = false - 1', () => {
+        const frag = new Fragment()
+        const group = frag.group()
+        group.rect(123.456, 234.567)
+        const spy = spyOn(Dom.prototype, 'svg').and.callThrough()
+
+        expect(frag.svg(false)).toBe('<g><rect width="123.456" height="234.567"></rect></g>')
+        expect(spy).toHaveBeenCalledWith(null, false)
+      })
+
+      it('calls parent method with outerHtml = false - 2', () => {
+        const frag = new Fragment()
+        const group = frag.group()
+        group.rect(123.456, 234.567)
+        const spy = spyOn(Dom.prototype, 'svg').and.callThrough()
+
+        expect(frag.svg(null, true)).toBe('<g><rect width="123.456" height="234.567"></rect></g>')
+        expect(spy).toHaveBeenCalledWith(null, false)
+      })
+    })
+
+  })
+})
index c209980a2d994b71baee76e00af05956ef538471..e5447526d815d1f296308a6551c7fd3a3c00b14c 100644 (file)
@@ -15,7 +15,8 @@ import {
   G,
   Gradient,
   Dom,
-  Path
+  Path,
+  Fragment
 } from '../../../src/main.js'
 
 import { mockAdopt, assignNewId, adopt } from '../../../src/utils/adopter.js'
@@ -120,6 +121,11 @@ describe('adopter.js', () => {
       expect(adopt(rect.node)).toBe(rect)
     })
 
+    it('creates Fragment when document fragment is passed', () => {
+      const frag = getWindow().document.createDocumentFragment()
+      expect(adopt(frag)).toEqual(any(Fragment))
+    })
+
     it('creates instance when node without instance is passed', () => {
       const rect = new Rect()
       const node = rect.node
index 0180c78fb677ec2f1afa4c55e9ee1631793b5339..c9e695310ad1046a1385b0bdb5462ce03e017746 100644 (file)
@@ -142,7 +142,8 @@ export default class Dom extends EventTarget {
   // matches the element vs a css selector
   matches (selector) {
     const el = this.node
-    return (el.matches || el.matchesSelector || el.msMatchesSelector || el.mozMatchesSelector || el.webkitMatchesSelector || el.oMatchesSelector).call(el, selector)
+    const matcher = el.matches || el.matchesSelector || el.msMatchesSelector || el.mozMatchesSelector || el.webkitMatchesSelector || el.oMatchesSelector || null
+    return matcher && matcher.call(el, selector)
   }
 
   // Returns the parent element instance
@@ -151,7 +152,6 @@ export default class Dom extends EventTarget {
 
     // check for parent
     if (!parent.node.parentNode) return null
-    if (parent.node.parentNode.nodeName === '#document' || parent.node.parentNode.nodeName === '#document-fragment') return null
 
     // get parent element
     parent = adopt(parent.node.parentNode)
@@ -159,11 +159,11 @@ export default class Dom extends EventTarget {
     if (!type) return parent
 
     // loop trough ancestors if type is given
-    while (parent) {
+    do {
       if (typeof type === 'string' ? parent.matches(type) : parent instanceof type) return parent
-      if (!parent.node.parentNode || parent.node.parentNode.nodeName === '#document' || parent.node.parentNode.nodeName === '#document-fragment') return null // #759, #720
-      parent = adopt(parent.node.parentNode)
-    }
+    } while ((parent = adopt(parent.node.parentNode)))
+
+    return parent
   }
 
   // Basically does the same as `add()` but returns the added element instead
@@ -197,7 +197,11 @@ export default class Dom extends EventTarget {
   // Replace this with element
   replace (element) {
     element = makeInstance(element)
-    this.node.parentNode.replaceChild(element.node, this.node)
+
+    if (this.node.parentNode) {
+      this.node.parentNode.replaceChild(element.node, this.node)
+    }
+
     return element
   }
 
@@ -206,14 +210,16 @@ export default class Dom extends EventTarget {
     const attrs = this.attr(map)
 
     for (const i in attrs) {
-      attrs[i] = Math.round(attrs[i] * factor) / factor
+      if (typeof attrs[i] === 'number') {
+        attrs[i] = Math.round(attrs[i] * factor) / factor
+      }
     }
 
     this.attr(attrs)
     return this
   }
 
-  // Import raw svg
+  // Import / Export raw svg
   svg (svgOrFn, outerHTML) {
     var well, len, fragment
 
diff --git a/src/elements/Fragment.js b/src/elements/Fragment.js
new file mode 100644 (file)
index 0000000..228e93f
--- /dev/null
@@ -0,0 +1,34 @@
+import Dom from './Dom.js'
+import { globals } from '../utils/window.js'
+import { register } from '../utils/adopter.js'
+import Svg from './Svg.js'
+
+class Fragment extends Dom {
+  constructor (node = globals.document.createDocumentFragment()) {
+    super(node)
+  }
+
+  // Import / Export raw svg
+  svg (svgOrFn, outerHTML) {
+    if (svgOrFn === false) {
+      outerHTML = false
+      svgOrFn = null
+    }
+
+    // act as getter if no svg string is given
+    if (svgOrFn == null || typeof svgOrFn === 'function') {
+      const wrapper = new Svg()
+      wrapper.add(this.node.cloneNode(true))
+
+      return wrapper.svg(svgOrFn, false)
+    }
+
+    // Act as setter if we got a string
+    return super.svg(svgOrFn, false)
+  }
+
+}
+
+register(Fragment, 'Fragment')
+
+export default Fragment
index 3162752386fce1432ec2379c377d0a99745afcd1..12ebc7b38ee22d49ea7d5c6becc00ec8f7a87b37 100644 (file)
@@ -18,6 +18,7 @@ import Dom from './elements/Dom.js'
 import Element from './elements/Element.js'
 import Ellipse from './elements/Ellipse.js'
 import EventTarget from './types/EventTarget.js'
+import Fragment from './elements/Fragment.js'
 import Gradient from './elements/Gradient.js'
 import Image from './elements/Image.js'
 import Line from './elements/Line.js'
@@ -96,6 +97,7 @@ export { default as Dom } from './elements/Dom.js'
 export { default as Element } from './elements/Element.js'
 export { default as Ellipse } from './elements/Ellipse.js'
 export { default as ForeignObject } from './elements/ForeignObject.js'
+export { default as Fragment } from './elements/Fragment.js'
 export { default as Gradient } from './elements/Gradient.js'
 export { default as G } from './elements/G.js'
 export { default as A } from './elements/A.js'
@@ -154,8 +156,7 @@ extend(EventTarget, getMethodsFor('EventTarget'))
 extend(Dom, getMethodsFor('Dom'))
 extend(Element, getMethodsFor('Element'))
 extend(Shape, getMethodsFor('Shape'))
-// extend(Element, getConstructor('Memory'))
-extend(Container, getMethodsFor('Container'))
+extend([ Container, Fragment ], getMethodsFor('Container'))
 
 extend(Runner, getMethodsFor('Runner'))
 
index b01683792c8a985df61c373df2ad9e67e5c13558..217aafbbb9c84020a87d66de4d445595b6e63d38 100644 (file)
@@ -53,6 +53,10 @@ export function adopt (node) {
   // make sure a node isn't already adopted
   if (node.instance instanceof Base) return node.instance
 
+  if (node.nodeName === '#document-fragment') {
+    return new elements.Fragment(node)
+  }
+
   // initialize variables
   var className = capitalize(node.nodeName || 'Dom')
 
index 72c6d7d56601cf5c44e9af2feab423c3c186c85c..8dd99e25758381732472ae4eed50d2635440294e 100644 (file)
@@ -970,7 +970,7 @@ declare module "@svgdotjs/svg.js" {
         attr(name: string, value: any, namespace?: string): this;\r
         attr(name: string): any;\r
         attr(obj: object): this;\r
-        attr(obj: object[]): object;\r
+        attr(obj: string[]): object;\r
 \r
         // prototype extend Selector in selector.js\r
         find(query: string): List<Element>\r