From d927f2d225d74fd3b3f41b298a19f6ba075702cf Mon Sep 17 00:00:00 2001 From: =?utf8?q?Ulrich-Matthias=20Sch=C3=A4fer?= Date: Wed, 8 Apr 2020 07:32:38 +1000 Subject: [PATCH] added Fragment, completed Dom Tests, fixed `matches()` for document fragments --- CHANGELOG.md | 4 +- spec/spec/elements/Dom.js | 322 ++++++++++++++++++++++++++++++++- spec/spec/elements/Fragment.js | 61 +++++++ spec/spec/utils/adopter.js | 8 +- src/elements/Dom.js | 24 ++- src/elements/Fragment.js | 34 ++++ src/main.js | 5 +- src/utils/adopter.js | 4 + svg.js.d.ts | 2 +- 9 files changed, 443 insertions(+), 21 deletions(-) create mode 100644 spec/spec/elements/Fragment.js create mode 100644 src/elements/Fragment.js diff --git a/CHANGELOG.md b/CHANGELOG.md index 1152156..83af8ea 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/spec/spec/elements/Dom.js b/spec/spec/elements/Dom.js index a1e2a6d..6689822 100644 --- a/spec/spec/elements/Dom.js +++ b/spec/spec/elements/Dom.js @@ -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 = '' + 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 = '' + 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('')).toBe(g) + }) + + it('imports a single element', () => { + const g = new G().svg('') + expect(g.children()).toEqual([ any(Rect) ]) + }) + + it('imports multiple elements', () => { + const g = new G().svg('') + 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('', 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('', 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('') + expect(canvas.svg()).toBe('') + }) + + it('returns the innerHtml when outerHtml = false', () => { + expect(rect.svg(false)).toBe('') + expect(canvas.svg(false)).toBe('') + }) + + it('runs a function on every exported node', () => { + expect(rect.svg((el) => el.round(1))).toBe('') + }) + + 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('') + expect(canvas.svg((el) => new G())).toBe('') // outer was replaced by an empty g + expect(canvas.svg((el) => { + if (el instanceof Rect) return new Circle() + if (el instanceof Svg) el.removeNamespace() + })).toBe('') + }) + + 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('') + }) + + 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('') + }) + + }) + + }) + + 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 index 0000000..c0f5f0f --- /dev/null +++ b/spec/spec/elements/Fragment.js @@ -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('', true) + expect(spy).toHaveBeenCalledWith('', 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('') + 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('') + expect(spy).toHaveBeenCalledWith(null, false) + }) + }) + + }) +}) diff --git a/spec/spec/utils/adopter.js b/spec/spec/utils/adopter.js index c209980..e544752 100644 --- a/spec/spec/utils/adopter.js +++ b/spec/spec/utils/adopter.js @@ -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 diff --git a/src/elements/Dom.js b/src/elements/Dom.js index 0180c78..c9e6953 100644 --- a/src/elements/Dom.js +++ b/src/elements/Dom.js @@ -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 index 0000000..228e93f --- /dev/null +++ b/src/elements/Fragment.js @@ -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 diff --git a/src/main.js b/src/main.js index 3162752..12ebc7b 100644 --- a/src/main.js +++ b/src/main.js @@ -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')) diff --git a/src/utils/adopter.js b/src/utils/adopter.js index b016837..217aafb 100644 --- a/src/utils/adopter.js +++ b/src/utils/adopter.js @@ -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') diff --git a/svg.js.d.ts b/svg.js.d.ts index 72c6d7d..8dd99e2 100644 --- a/svg.js.d.ts +++ b/svg.js.d.ts @@ -970,7 +970,7 @@ declare module "@svgdotjs/svg.js" { attr(name: string, value: any, namespace?: string): this; attr(name: string): any; attr(obj: object): this; - attr(obj: object[]): object; + attr(obj: string[]): object; // prototype extend Selector in selector.js find(query: string): List -- 2.39.5