/* * LeaderLine * https://anseki.github.io/leader-line/ * * Copyright (c) 2021 anseki * Licensed under the MIT license. */ /* exported LeaderLine */ /* eslint no-underscore-dangle: [2, {"allow": ["_id"]}] */ /* global traceLog:false */ ;var LeaderLine = (function() { // eslint-disable-line no-extra-semi 'use strict'; /** * An object that simulates `DOMRect` to indicate a bounding-box. * @typedef {Object} BBox * @property {(number|null)} left - ScreenCTM * @property {(number|null)} top - ScreenCTM * @property {(number|null)} right - ScreenCTM * @property {(number|null)} bottom - ScreenCTM * @property {(number|null)} x - Substitutes for left * @property {(number|null)} y - Substitutes for top * @property {(number|null)} width * @property {(number|null)} height */ /** * An object that has coordinates of ScreenCTM. * @typedef {Object} Point * @property {number} x * @property {number} y */ /** * @typedef {Object} AnimOptions * @property {number} duration * @property {(string|number[])} timing - FUNC_KEYS or [x1, y1, x2, y2] */ var APP_ID = 'leader-line', SOCKET_TOP = 1, SOCKET_RIGHT = 2, SOCKET_BOTTOM = 3, SOCKET_LEFT = 4, SOCKET_KEY_2_ID = {top: SOCKET_TOP, right: SOCKET_RIGHT, bottom: SOCKET_BOTTOM, left: SOCKET_LEFT}, PATH_STRAIGHT = 1, PATH_ARC = 2, PATH_FLUID = 3, PATH_MAGNET = 4, PATH_GRID = 5, PATH_KEY_2_ID = { straight: PATH_STRAIGHT, arc: PATH_ARC, fluid: PATH_FLUID, magnet: PATH_MAGNET, grid: PATH_GRID}, /** * @typedef {Object} SymbolConf * @property {string} elmId * @property {BBox} bBox * @property {number} widthR * @property {number} heightR * @property {number} bCircle * @property {number} sideLen * @property {number} backLen * @property {number} overhead * @property {(boolean|null)} noRotate * @property {(number|null)} outlineBase * @property {(number|null)} outlineMax */ /** @typedef {{symbolId: string, SymbolConf}} SYMBOLS */ PLUG_BEHIND = 'behind', DEFS_ID = APP_ID + '-defs', /* [DEBUG/] DEFS_HTML = @INCLUDE[code:DEFS_HTML]@, SYMBOLS = @INCLUDE[code:SYMBOLS]@, PLUG_KEY_2_ID = @INCLUDE[code:PLUG_KEY_2_ID]@, PLUG_2_SYMBOL = @INCLUDE[code:PLUG_2_SYMBOL]@, DEFAULT_END_PLUG = @INCLUDE[code:DEFAULT_END_PLUG]@, [DEBUG/] */ // [DEBUG] DEFS_HTML = window.DEFS_HTML, SYMBOLS = window.SYMBOLS, PLUG_KEY_2_ID = window.PLUG_KEY_2_ID, PLUG_2_SYMBOL = window.PLUG_2_SYMBOL, DEFAULT_END_PLUG = window.DEFAULT_END_PLUG, // [/DEBUG] SOCKET_IDS = [SOCKET_TOP, SOCKET_RIGHT, SOCKET_BOTTOM, SOCKET_LEFT], KEYWORD_AUTO = 'auto', BBOX_PROP = {x: 'left', y: 'top', width: 'width', height: 'height'}, MIN_GRAVITY = 80, MIN_GRAVITY_SIZE = 4, MIN_GRAVITY_R = 5, MIN_OH_GRAVITY = 120, MIN_OH_GRAVITY_OH = 8, MIN_OH_GRAVITY_R = 3.75, MIN_ADJUST_LEN = 10, MIN_GRID_LEN = 30, CIRCLE_CP = 0.5522847, CIRCLE_8_RAD = 1 / 4 * Math.PI, RE_PERCENT = /^\s*(\-?[\d\.]+)\s*(\%)?\s*$/, SVG_NS = 'http://www.w3.org/2000/svg', IS_EDGE = '-ms-scroll-limit' in document.documentElement.style && '-ms-ime-align' in document.documentElement.style && !window.navigator.msPointerEnabled, IS_TRIDENT = !IS_EDGE && !!document.uniqueID, // Future Edge might support `document.uniqueID`. IS_GECKO = 'MozAppearance' in document.documentElement.style, IS_BLINK = !IS_EDGE && !IS_GECKO && // Edge has `window.chrome`, and future Gecko might have that. !!window.chrome && !!window.CSS, IS_WEBKIT = !IS_EDGE && !IS_TRIDENT && !IS_GECKO && !IS_BLINK && // Some engines support `webkit-*` properties. !window.chrome && 'WebkitAppearance' in document.documentElement.style, SHAPE_GAP = IS_TRIDENT || IS_EDGE ? 0.2 : 0.1, DEFAULT_OPTIONS = { path: PATH_FLUID, lineColor: 'coral', lineSize: 4, plugSE: [PLUG_BEHIND, DEFAULT_END_PLUG], plugSizeSE: [1, 1], lineOutlineEnabled: false, lineOutlineColor: 'indianred', lineOutlineSize: 0.25, plugOutlineEnabledSE: [false, false], plugOutlineSizeSE: [1, 1] }, isObject = (function() { var toString = {}.toString, fnToString = {}.hasOwnProperty.toString, objFnString = fnToString.call(Object); return function(obj) { var proto, constructor; return obj && toString.call(obj) === '[object Object]' && (!(proto = Object.getPrototypeOf(obj)) || (constructor = proto.hasOwnProperty('constructor') && proto.constructor) && typeof constructor === 'function' && fnToString.call(constructor) === objFnString); }; })(), isFinite = Number.isFinite || function(value) { return typeof value === 'number' && window.isFinite(value); }, /* [DEBUG/] anim = @INCLUDE[code:anim]@, [DEBUG/] */ anim = window.anim, // [DEBUG/] /* [DEBUG/] pathDataPolyfill = @INCLUDE[code:pathDataPolyfill]@, [DEBUG/] */ pathDataPolyfill = window.pathDataPolyfill, // [DEBUG/] /* [DEBUG/] AnimEvent = @INCLUDE[code:AnimEvent]@, [DEBUG/] */ AnimEvent = window.AnimEvent, // [DEBUG/] /** @typedef {{hasSE, hasProps, iniValue}} StatConf */ /** @type {{statId: string, StatConf}} */ STATS = { line_altColor: {iniValue: false}, line_color: {}, line_colorTra: {iniValue: false}, line_strokeWidth: {}, plug_enabled: {iniValue: false}, plug_enabledSE: {hasSE: true, iniValue: false}, plug_plugSE: {hasSE: true, iniValue: PLUG_BEHIND}, plug_colorSE: {hasSE: true}, plug_colorTraSE: {hasSE: true, iniValue: false}, plug_markerWidthSE: {hasSE: true}, plug_markerHeightSE: {hasSE: true}, lineOutline_enabled: {iniValue: false}, lineOutline_color: {}, lineOutline_colorTra: {iniValue: false}, lineOutline_strokeWidth: {}, lineOutline_inStrokeWidth: {}, plugOutline_enabledSE: {hasSE: true, iniValue: false}, plugOutline_plugSE: {hasSE: true, iniValue: PLUG_BEHIND}, plugOutline_colorSE: {hasSE: true}, plugOutline_colorTraSE: {hasSE: true, iniValue: false}, plugOutline_strokeWidthSE: {hasSE: true}, plugOutline_inStrokeWidthSE: {hasSE: true}, position_socketXYSE: {hasSE: true, hasProps: true}, position_plugOverheadSE: {hasSE: true}, position_path: {}, position_lineStrokeWidth: {}, position_socketGravitySE: {hasSE: true}, path_pathData: {}, path_edge: {hasProps: true}, viewBox_bBox: {hasProps: true}, viewBox_plugBCircleSE: {hasSE: true}, lineMask_enabled: {iniValue: false}, lineMask_outlineMode: {iniValue: false}, lineMask_x: {}, lineMask_y: {}, lineOutlineMask_x: {}, lineOutlineMask_y: {}, maskBGRect_x: {}, maskBGRect_y: {}, capsMaskAnchor_enabledSE: {hasSE: true, iniValue: false}, capsMaskAnchor_pathDataSE: {hasSE: true}, capsMaskAnchor_strokeWidthSE: {hasSE: true}, capsMaskMarker_enabled: {iniValue: false}, capsMaskMarker_enabledSE: {hasSE: true, iniValue: false}, capsMaskMarker_plugSE: {hasSE: true, iniValue: PLUG_BEHIND}, capsMaskMarker_markerWidthSE: {hasSE: true}, capsMaskMarker_markerHeightSE: {hasSE: true}, caps_enabled: {iniValue: false}, attach_plugSideLenSE: {hasSE: true}, attach_plugBackLenSE: {hasSE: true} }, SHOW_STATS = { show_on: {}, show_effect: {}, show_animOptions: {}, show_animId: {}, show_inAnim: {} }, EFFECTS, SHOW_EFFECTS, ATTACHMENTS, LeaderLineAttachment, DEFAULT_SHOW_EFFECT = 'fade', isAttachment, removeAttachment, delayedProcs = [], timerDelayedProc, /** @type {Object.<_id: number, props>} */ insProps = {}, insId = 0, /** @type {Object.<_id: number, props>} */ insAttachProps = {}, insAttachId = 0, svg2SupportedReverse, svg2SupportedPaintOrder, svg2SupportedDropShadow; // Supported SVG 2 features // [DEBUG] window.insProps = insProps; window.insAttachProps = insAttachProps; window.isObject = isObject; window.IS_TRIDENT = IS_TRIDENT; window.IS_BLINK = IS_BLINK; window.IS_GECKO = IS_GECKO; window.IS_EDGE = IS_EDGE; window.IS_WEBKIT = IS_WEBKIT; window.engineFlags = function(flags) { if (typeof flags.IS_TRIDENT === 'boolean') { window.IS_TRIDENT = IS_TRIDENT = flags.IS_TRIDENT; } if (typeof flags.IS_BLINK === 'boolean') { window.IS_BLINK = IS_BLINK = flags.IS_BLINK; } if (typeof flags.IS_GECKO === 'boolean') { window.IS_GECKO = IS_GECKO = flags.IS_GECKO; } if (typeof flags.IS_EDGE === 'boolean') { window.IS_EDGE = IS_EDGE = flags.IS_EDGE; } if (typeof flags.IS_WEBKIT === 'boolean') { window.IS_WEBKIT = IS_WEBKIT = flags.IS_WEBKIT; } }; // [/DEBUG] function hasChanged(a, b) { var typeA, keysA; return typeof a !== typeof b || (typeA = isObject(a) ? 'obj' : Array.isArray(a) ? 'array' : '') !== (isObject(b) ? 'obj' : Array.isArray(b) ? 'array' : '') || ( typeA === 'obj' ? hasChanged((keysA = Object.keys(a).sort()), Object.keys(b).sort()) || keysA.some(function(prop) { return hasChanged(a[prop], b[prop]); }) : typeA === 'array' ? a.length !== b.length || a.some(function(aVal, i) { return hasChanged(aVal, b[i]); }) : a !== b ); } window.hasChanged = hasChanged; // [DEBUG/] function copyTree(obj) { return !obj ? obj : isObject(obj) ? Object.keys(obj).reduce(function(copyObj, key) { copyObj[key] = copyTree(obj[key]); return copyObj; }, {}) : Array.isArray(obj) ? obj.map(copyTree) : obj; } window.copyTree = copyTree; // [DEBUG/] /** * Parse and get an alpha channel in color notation. * @param {string} color - A color notation such as `'rgba(10, 20, 30, 0.6)'`. * @returns {Array} Alpha channel ([0, 1]) such as `0.6`, and base color. e.g. [0.6, 'rgb(10, 20, 30)'] */ function getAlpha(color) { var matches, func, args, alpha = 1, baseColor = (color = (color + '').trim()); function parseAlpha(value) { var alpha = 1, matches = RE_PERCENT.exec(value); if (matches) { alpha = parseFloat(matches[1]); if (matches[2]) { alpha = alpha >= 0 && alpha <= 100 ? alpha / 100 : 1; } else if (alpha < 0 || alpha > 1) { alpha = 1; } } return alpha; } // Unsupported: `currentcolor`, `color()`, `deprecated-system-color` if ((matches = /^(rgba|hsla|hwb|gray|device\-cmyk)\s*\(([\s\S]+)\)$/i.exec(color))) { func = matches[1].toLowerCase(); args = matches[2].trim().split(/\s*,\s*/); if (func === 'rgba' && args.length === 4) { alpha = parseAlpha(args[3]); baseColor = 'rgb(' + args.slice(0, 3).join(', ') + ')'; } else if (func === 'hsla' && args.length === 4) { alpha = parseAlpha(args[3]); baseColor = 'hsl(' + args.slice(0, 3).join(', ') + ')'; } else if (func === 'hwb' && args.length === 4) { alpha = parseAlpha(args[3]); baseColor = 'hwb(' + args.slice(0, 3).join(', ') + ')'; } else if (func === 'gray' && args.length === 2) { alpha = parseAlpha(args[1]); baseColor = 'gray(' + args[0] + ')'; } else if (func === 'device-cmyk' && args.length >= 5) { alpha = parseAlpha(args[4]); baseColor = 'device-cmyk(' + args.slice(0, 4).join(', ') + ')'; // omit } } else if ((matches = /^\#(?:([\da-f]{6})([\da-f]{2})|([\da-f]{3})([\da-f]))$/i.exec(color))) { if (matches[1]) { alpha = parseInt(matches[2], 16) / 255; baseColor = '#' + matches[1]; } else { alpha = parseInt(matches[4] + matches[4], 16) / 255; baseColor = '#' + matches[3]; } } else if (color.toLocaleLowerCase() === 'transparent') { alpha = 0; } return [alpha, baseColor]; } window.getAlpha = getAlpha; // [DEBUG/] /** * Add `mouseenter` and `mouseleave` event listeners to the element. * @param {Element} element - Target element. * @param {Function} enter - Event listener. * @param {Function} leave - Event listener. * @returns {Function} Function that removes the added listeners. */ function mouseEnterLeave(element, enter, leave) { var over, out; if ('onmouseenter' in element && 'onmouseleave' in element) { // Supported element.addEventListener('mouseenter', enter, false); element.addEventListener('mouseleave', leave, false); return function() { element.removeEventListener('mouseenter', enter, false); element.removeEventListener('mouseleave', leave, false); }; } else { // Unsupported console.warn('mouseenter and mouseleave events polyfill is enabled.'); over = function(event) { /* eslint-disable no-invalid-this */ if (!event.relatedTarget || event.relatedTarget !== this && !(this.compareDocumentPosition(event.relatedTarget) & Node.DOCUMENT_POSITION_CONTAINED_BY)) { enter.apply(this, arguments); } /* eslint-enable no-invalid-this */ }; element.addEventListener('mouseover', over); out = function(event) { /* eslint-disable no-invalid-this */ if (!event.relatedTarget || event.relatedTarget !== this && !(this.compareDocumentPosition(event.relatedTarget) & Node.DOCUMENT_POSITION_CONTAINED_BY)) { leave.apply(this, arguments); } /* eslint-enable no-invalid-this */ }; element.addEventListener('mouseout', out); return function() { element.removeEventListener('mouseover', over, false); element.removeEventListener('mouseout', out, false); }; } } window.mouseEnterLeave = mouseEnterLeave; // [DEBUG/] function isElement(element) { // The checking the interface may not be required. // var win, doc; // return !!(element && (doc = element.ownerDocument) && (win = doc.defaultView) && win.HTMLElement && // element instanceof win.HTMLElement); return !!(element && element.nodeType === Node.ELEMENT_NODE && typeof element.getBoundingClientRect === 'function'); } window.isElement = isElement; // [DEBUG/] /** * Get an element's bounding-box that contains coordinates relative to the element's document or window. * @param {Element} element - Target element. * @param {boolean} [relWindow] - Whether it's relative to the element's window, or document (i.e. ``). * @returns {(BBox|null)} A bounding-box or null when failed. */ function getBBox(element, relWindow) { var bBox = {}, rect, prop, doc, win; if (!(doc = element.ownerDocument)) { console.error('Cannot get document that contains the element.'); return null; } if (element.compareDocumentPosition(doc) & Node.DOCUMENT_POSITION_DISCONNECTED) { console.error('A disconnected element was passed.'); return null; } rect = element.getBoundingClientRect(); for (prop in rect) { bBox[prop] = rect[prop]; } // eslint-disable-line guard-for-in if (!relWindow) { if (!(win = doc.defaultView)) { console.error('Cannot get window that contains the element.'); return null; } bBox.left += win.pageXOffset; bBox.right += win.pageXOffset; bBox.top += win.pageYOffset; bBox.bottom += win.pageYOffset; } return bBox; } window.getBBox = getBBox; // [DEBUG/] /** * Get distance between an element's bounding-box and its content (`