/** 
 | 
 * @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; 
 | 
    } 
 | 
  
 | 
} 
 |