Definable.ts 6.25 KB
/**
 * @file Manages elements that can be defined in <defs> in SVG,
 *       e.g., gradients, clip path, etc.
 * @author Zhang Wenli
 */

import {createElement} from '../../svg/core';
import * as zrUtil from '../../core/util';
import Displayable from '../../graphic/Displayable';


const MARK_UNUSED = '0';
const MARK_USED = '1';

/**
 * Manages elements that can be defined in <defs> in SVG,
 * e.g., gradients, clip path, etc.
 */
export default class Definable {

    nextId = 0

    protected _zrId: number
    protected _svgRoot: SVGElement
    protected _tagNames: string[]
    protected _markLabel: string
    protected _domName: string = '_dom'

    constructor(
        zrId: number,   // zrender instance id
        svgRoot: SVGElement,        // root of SVG document
        tagNames: string | string[],    // possible tag names
        markLabel: string,  // label name to make if the element
        domName?: string
    ) {
        this._zrId = zrId;
        this._svgRoot = svgRoot;
        this._tagNames = typeof tagNames === 'string' ? [tagNames] : tagNames;
        this._markLabel = markLabel;

        if (domName) {
            this._domName = domName;
        }
    }

    /**
     * Get the <defs> tag for svgRoot; optionally creates one if not exists.
     *
     * @param isForceCreating if need to create when not exists
     * @return SVG <defs> element, null if it doesn't
     * exist and isForceCreating is false
     */
    getDefs(isForceCreating?: boolean): SVGDefsElement {
        let svgRoot = this._svgRoot;
        let defs = this._svgRoot.getElementsByTagName('defs');
        if (defs.length === 0) {
            // Not exist
            if (isForceCreating) {
                let defs = svgRoot.insertBefore(
                    createElement('defs'), // Create new tag
                    svgRoot.firstChild // Insert in the front of svg
                ) as SVGDefsElement;
                if (!defs.contains) {
                    // IE doesn't support contains method
                    defs.contains = function (el) {
                        const children = defs.children;
                        if (!children) {
                            return false;
                        }
                        for (let i = children.length - 1; i >= 0; --i) {
                            if (children[i] === el) {
                                return true;
                            }
                        }
                        return false;
                    };
                }
                return defs;
            }
            else {
                return null;
            }
        }
        else {
            return defs[0];
        }
    }


    /**
     * Update DOM element if necessary.
     *
     * @param element style element. e.g., for gradient,
     *                                it may be '#ccc' or {type: 'linear', ...}
     * @param onUpdate update callback
     */
    doUpdate<T>(target: T, onUpdate?: (target: T) => void) {
        if (!target) {
            return;
        }

        const defs = this.getDefs(false);
        if ((target as any)[this._domName] && defs.contains((target as any)[this._domName])) {
            // Update DOM
            if (typeof onUpdate === 'function') {
                onUpdate(target);
            }
        }
        else {
            // No previous dom, create new
            const dom = this.add(target);
            if (dom) {
                (target as any)[this._domName] = dom;
            }
        }
    }

    add(target: any): SVGElement {
        return null;
    }

    /**
     * Add gradient dom to defs
     *
     * @param dom DOM to be added to <defs>
     */
    addDom(dom: SVGElement) {
        const defs = this.getDefs(true);
        if (dom.parentNode !== defs) {
            defs.appendChild(dom);
        }
    }


    /**
     * Remove DOM of a given element.
     *
     * @param target Target where to attach the dom
     */
    removeDom<T>(target: T) {
        const defs = this.getDefs(false);
        if (defs && (target as any)[this._domName]) {
            defs.removeChild((target as any)[this._domName]);
            (target as any)[this._domName] = null;
        }
    }


    /**
     * Get DOMs of this element.
     *
     * @return doms of this defineable elements in <defs>
     */
    getDoms() {
        const defs = this.getDefs(false);
        if (!defs) {
            // No dom when defs is not defined
            return [];
        }

        let doms: SVGElement[] = [];
        zrUtil.each(this._tagNames, function (tagName) {
            const tags = defs.getElementsByTagName(tagName) as HTMLCollectionOf<SVGElement>;
            // Note that tags is HTMLCollection, which is array-like
            // rather than real array.
            // So `doms.concat(tags)` add tags as one object.
            for (let i = 0; i < tags.length; i++) {
                doms.push(tags[i]);
            }
        });

        return doms;
    }


    /**
     * Mark DOMs to be unused before painting, and clear unused ones at the end
     * of the painting.
     */
    markAllUnused() {
        const doms = this.getDoms();
        const that = this;
        zrUtil.each(doms, function (dom) {
            (dom as any)[that._markLabel] = MARK_UNUSED;
        });
    }


    /**
     * Mark a single DOM to be used.
     *
     * @param dom DOM to mark
     */
    markDomUsed(dom: SVGElement) {
        dom && ((dom as any)[this._markLabel] = MARK_USED);
    };

    markDomUnused(dom: SVGElement) {
        dom && ((dom as any)[this._markLabel] = MARK_UNUSED);
    };

    isDomUnused(dom: SVGElement) {
        return dom && (dom as any)[this._markLabel] !== MARK_USED;
    }

    /**
     * Remove unused DOMs defined in <defs>
     */
    removeUnused() {
        const defs = this.getDefs(false);
        if (!defs) {
            // Nothing to remove
            return;
        }

        const doms = this.getDoms();
        zrUtil.each(doms, (dom) => {
            if (this.isDomUnused(dom)) {
                // Remove gradient
                defs.removeChild(dom);
            }
        });
    }

    /**
     * Get SVG element.
     *
     * @param displayable displayable element
     * @return SVG element
     */
    getSvgElement(displayable: Displayable): SVGElement {
        return displayable.__svgEl;
    }

}