`

rc-dialog

 
阅读更多

rc-dialog用于绘制对话框,对话框的可见逻辑由父组件设定。

不包含基于react-native实现的modal组件。

 

DialogWrap.js

'use strict';

Object.defineProperty(exports, "__esModule", {
    value: true
});

var _react = require('react');
var _react2 = _interopRequireDefault(_react);

var _Dialog = require('./Dialog');
var _Dialog2 = _interopRequireDefault(_Dialog);

// 提供在document中创建div,并将组件的renderComponent渲染的元素挂载到该div下的能力  
// 同时使组件实例的_component属性指向renderComponent方法渲染出来的组件实例  
var _getContainerRenderMixin = require('rc-util/lib/getContainerRenderMixin');
var _getContainerRenderMixin2 = _interopRequireDefault(_getContainerRenderMixin);

function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { "default": obj }; }

var __assign = undefined && undefined.__assign || Object.assign || function (t) {
    for (var s, i = 1, n = arguments.length; i < n; i++) {
        s = arguments[i];
        for (var p in s) {
            if (Object.prototype.hasOwnProperty.call(s, p)) t[p] = s[p];
        }
    }
    return t;
};

// DialogWrap直接将获得的props注入Dialog组件,同时Dialog元素作为新插入文档的节点的子元素
var DialogWrap = _react2["default"].createClass({
    displayName: 'DialogWrap',

    mixins: [(0, _getContainerRenderMixin2["default"])({
        isVisible: function isVisible(instance) {
            return instance.props.visible;
        },

        autoDestroy: false,
        getComponent: function getComponent(instance, extra) {
            return _react2["default"].createElement(
                _Dialog2["default"], 
                __assign({}, instance.props, extra, { key: "dialog" })
            );
        }
    })],
    getDefaultProps: function getDefaultProps() {
        return {
            visible: false
        };
    },
    shouldComponentUpdate: function shouldComponentUpdate(_ref) {
        var visible = _ref.visible;

        return !!(this.props.visible || visible);
    },
    componentWillUnmount: function componentWillUnmount() {
        if (this.props.visible) {
            this.renderComponent({
                afterClose: this.removeContainer,
                onClose: function onClose() {},

                visible: false
            });
        } else {
            this.removeContainer();
        }
    },
    getElement: function getElement(part) {
        return this._component.getElement(part);
    },
    render: function render() {
        return null;
    }
});
exports["default"] = DialogWrap;
module.exports = exports['default'];

 

Dialog.js

'use strict';

Object.defineProperty(exports, "__esModule", {
    value: true
});

var _react = require('react');
var _react2 = _interopRequireDefault(_react);

var _reactDom = require('react-dom');
var _reactDom2 = _interopRequireDefault(_reactDom);

var _KeyCode = require('rc-util/lib/KeyCode');
var _KeyCode2 = _interopRequireDefault(_KeyCode);

var _rcAnimate = require('rc-animate');
var _rcAnimate2 = _interopRequireDefault(_rcAnimate);

var _LazyRenderBox = require('./LazyRenderBox');
var _LazyRenderBox2 = _interopRequireDefault(_LazyRenderBox);

// 计算滚动条宽度
var _getScrollBarSize = require('rc-util/lib/getScrollBarSize');
var _getScrollBarSize2 = _interopRequireDefault(_getScrollBarSize);

var _objectAssign = require('object-assign');
var _objectAssign2 = _interopRequireDefault(_objectAssign);

function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { "default": obj }; }

var __assign = undefined && undefined.__assign || Object.assign || function (t) {
    for (var s, i = 1, n = arguments.length; i < n; i++) {
        s = arguments[i];
        for (var p in s) {
            if (Object.prototype.hasOwnProperty.call(s, p)) t[p] = s[p];
        }
    }
    return t;
};

var uuid = 0;
var openCount = 0;
/* eslint react/no-is-mounted:0 */
function noop() {}
function setTransformOrigin(node, value) {
    var style = node.style;
    ['Webkit', 'Moz', 'Ms', 'ms'].forEach(function (prefix) {
        style[prefix + 'TransformOrigin'] = value;
    });
    style['transformOrigin'] = value;
}
// 由文档滚动调整元素的top、left样式
function offset(el) {
    var rect = el.getBoundingClientRect();
    var pos = {
        left: rect.left,
        top: rect.top
    };
    var doc = el.ownerDocument;
    var w = doc.defaultView || doc.parentWindow;
    pos.left += getScroll(w);
    pos.top += getScroll(w, true);
    return pos;
}
// 文档滚动时产生的偏移量
function getScroll(w, top) {
    var ret = w['page' + (top ? 'Y' : 'X') + 'Offset'];
    var method = 'scroll' + (top ? 'Top' : 'Left');
    if (typeof ret !== 'number') {
        var d = w.document;
        ret = d.documentElement[method];
        if (typeof ret !== 'number') {
            ret = d.body[method];
        }
    }
    return ret;
}

// 对话框居中效果由样式设定,以及显示的动效都由css设置(针对props.mousePosition)
var Dialog = _react2["default"].createClass({
    displayName: 'Dialog',
    getDefaultProps: function getDefaultProps() {
        return {
            afterClose: noop,// 对话框移除时执行的回调函数
            className: '',// 样式类
            prefixCls: 'rc-dialog',// 样式类前缀
            visible: false,// 由父组件操控对话框的显隐
            mask: true,// 是否显示蒙层
            closable: true,// 是否显示叉按钮
            keyboard: true,// esc按键是否执行this.props.onClick方法
            maskClosable: true,// 点击蒙层是否执行this.props.onClick方法
            onClose: noop// esc按键、点击叉按钮时触发执行,由父组件经营关闭对话框的操作
            // width、height 对话框的宽高
            // style 对话框的样式
            // title 对话框的标题,reactNode
            // children 对话框的内容体
            // footer 对话框内的尾部元素,reactNode
            // bodyProps 对话框内容体的props
            // wrapProps 对话框包裹元素的props
            // wrapStyle 对话框包裹元素的style
            // zIndex 对话框包裹元素的zIndex
            // transitionName、animation 对话框相关css动效

            // maskProps、maskStyle、maskTransitionName、maskAnimation 蒙层相关

            // mousePosition 设置对话框位置随鼠标x、y坐标调整
        };
    },
    componentWillMount: function componentWillMount() {
        this.titleId = 'rcDialogTitle' + uuid++;
    },
    componentDidMount: function componentDidMount() {
        this.componentDidUpdate({});
    },
    componentDidUpdate: function componentDidUpdate(prevProps) {
        var props = this.props;
        var mousePosition = this.props.mousePosition;
        if (props.visible) {
            // first show
            if (!prevProps.visible) {
                this.openTime = Date.now();
                this.lastOutSideFocusNode = document.activeElement;

                // 若文档宽度超过浏览器窗口宽度,为document.body增设右边距
                this.addScrollingEffect();
                this.refs.wrap.focus();
                var dialogNode = _reactDom2["default"].findDOMNode(this.refs.dialog);

                // 由鼠标位置调整对话框的top、left样式
                if (mousePosition) {
                    var elOffset = offset(dialogNode);
                    setTransformOrigin(dialogNode, mousePosition.x - elOffset.left + 'px ' + (mousePosition.y - elOffset.top) + 'px');
                } else {
                    setTransformOrigin(dialogNode, '');
                }
            }
        } else if (prevProps.visible) {
            if (props.mask && this.lastOutSideFocusNode) {
                try {
                    this.lastOutSideFocusNode.focus();
                } catch (e) {
                    this.lastOutSideFocusNode = null;
                }
                this.lastOutSideFocusNode = null;
            }
        }
    },
    componentWillUnmount: function componentWillUnmount() {
        if (this.props.visible) {
            this.removeScrollingEffect();
        }
    },

    // 对话框移除时执行,rc-animation机制
    onAnimateLeave: function onAnimateLeave() {
        // need demo?
        // https://github.com/react-component/dialog/pull/28
        if (this.refs.wrap) {
            this.refs.wrap.style.display = 'none';
        }
        this.removeScrollingEffect();
        this.props.afterClose();
    },

    // 点击蒙层执行this.props.onClick方法
    onMaskClick: function onMaskClick(e) {
        // android trigger click on open (fastclick??)
        if (Date.now() - this.openTime < 300) {
            return;
        }
        if (e.target === e.currentTarget) {
            this.close(e);
        }
    },
    // ESC键执行this.props.onClick方法,table键切换焦点
    onKeyDown: function onKeyDown(e) {
        var props = this.props;
        if (props.keyboard && e.keyCode === _KeyCode2["default"].ESC) {
            this.close(e);
        }
        // keep focus inside dialog
        if (props.visible) {
            if (e.keyCode === _KeyCode2["default"].TAB) {
                var activeElement = document.activeElement;
                var dialogRoot = this.refs.wrap;
                var sentinel = this.refs.sentinel;
                if (e.shiftKey) {
                    if (activeElement === dialogRoot) {
                        sentinel.focus();
                    }
                } else if (activeElement === this.refs.sentinel) {
                    dialogRoot.focus();
                }
            }
        }
    },

    // 获取对话框元素
    getDialogElement: function getDialogElement() {
        var props = this.props;
        var closable = props.closable;
        var prefixCls = props.prefixCls;
        var dest = {};

        // 获取对话框的宽高
        if (props.width !== undefined) {
            dest.width = props.width;
        }
        if (props.height !== undefined) {
            dest.height = props.height;
        }

        // 获取对话框的尾
        var footer = void 0;
        if (props.footer) {
            footer = _react2["default"].createElement(
                "div", 
                { className: prefixCls + '-footer', ref: "footer" }, 
                props.footer
            );
        }

        // 获取对话框的头
        var header = void 0;
        if (props.title) {
            header = _react2["default"].createElement(
                "div", 
                { className: prefixCls + '-header', ref: "header" }, 
                _react2["default"].createElement(
                    "div", 
                    { className: prefixCls + '-title', id: this.titleId }, 
                    props.title
                )
            );
        }

        // 叉按钮
        var closer = void 0;
        if (closable) {
            closer = _react2["default"].createElement(
                "button", 
                { onClick: this.close, "aria-label": "Close", className: prefixCls + '-close' }, 
                _react2["default"].createElement("span", { className: prefixCls + '-close-x' })
            );
        }

        var style = (0, _objectAssign2["default"])({}, props.style, dest);
        var transitionName = this.getTransitionName();

        var dialogElement = _react2["default"].createElement(
            _LazyRenderBox2["default"], 
            { 
                key: "dialog-element", 
                role: "document", 
                ref: "dialog", 
                style: style, 
                className: prefixCls + ' ' + (props.className || ''), 
                visible: props.visible 
            }, 
            _react2["default"].createElement(
                "div", 
                { className: prefixCls + '-content' }, 
                closer, 
                header, 
                _react2["default"].createElement(
                    "div", 
                    __assign({ 
                        className: prefixCls + '-body', 
                        style: props.bodyStyle, 
                        ref: "body" 
                    }, props.bodyProps), 
                    props.children
                ), 
                footer
            ), 
            _react2["default"].createElement(
                "div", 
                { 
                    tabIndex: 0, 
                    ref: "sentinel", 
                    style: { width: 0, height: 0, overflow: 'hidden' } 
                }, 
                "sentinel"
            )
        );

        return _react2["default"].createElement(
            _rcAnimate2["default"], 
            { 
                key: "dialog", 
                showProp: "visible",// 控制子元素显示的props属性
                onLeave: this.onAnimateLeave,// 子元素移除触发执行的方法
                transitionName: transitionName, 
                component: "", 
                transitionAppear: true 
            }, 
            dialogElement
        );
    },
    
    getTransitionName: function getTransitionName() {
        var props = this.props;
        var transitionName = props.transitionName;
        var animation = props.animation;
        if (!transitionName && animation) {
            transitionName = props.prefixCls + '-' + animation;
        }
        return transitionName;
    },

    getZIndexStyle: function getZIndexStyle() {
        var style = {};
        var props = this.props;
        if (props.zIndex !== undefined) {
            style.zIndex = props.zIndex;
        }
        return style;
    },
    // 获取对话框包裹元素的样式
    getWrapStyle: function getWrapStyle() {
        return (0, _objectAssign2["default"])({}, this.getZIndexStyle(), this.props.wrapStyle);
    },

    // 获取蒙层的样式
    getMaskStyle: function getMaskStyle() {
        return (0, _objectAssign2["default"])({}, this.getZIndexStyle(), this.props.maskStyle);
    },
    // 获取蒙层元素
    getMaskElement: function getMaskElement() {
        var props = this.props;
        var maskElement = void 0;
        if (props.mask) {
            var maskTransition = this.getMaskTransitionName();
            maskElement = _react2["default"].createElement(
                _LazyRenderBox2["default"], 
                __assign({ 
                    style: this.getMaskStyle(), 
                    key: "mask", 
                    className: props.prefixCls + '-mask', 
                    hiddenClassName: props.prefixCls + '-mask-hidden', 
                    visible: props.visible 
                }, props.maskProps)
            );

            if (maskTransition) {
                maskElement = _react2["default"].createElement(
                    _rcAnimate2["default"], 
                    { 
                        key: "mask", 
                        showProp: "visible", 
                        transitionAppear: true, 
                        component: "", 
                        transitionName: maskTransition 
                    }, 
                    maskElement
                );
            }
        }
        return maskElement;
    },
    getMaskTransitionName: function getMaskTransitionName() {
        var props = this.props;
        var transitionName = props.maskTransitionName;
        var animation = props.maskAnimation;
        if (!transitionName && animation) {
            transitionName = props.prefixCls + '-' + animation;
        }
        return transitionName;
    },

    getElement: function getElement(part) {
        return this.refs[part];
    },

    // 若文档宽度超过浏览器窗口宽度,为document.body增设右边距,通过openCount实现单次挂载时只增设一次
    addScrollingEffect: function addScrollingEffect() {
        openCount++;
        if (openCount !== 1) {
            return;
        }
        this.checkScrollbar();
        this.setScrollbar();
        document.body.style.overflow = 'hidden';
        // this.adjustDialog();
    },
    // 移除document.body的右边距
    removeScrollingEffect: function removeScrollingEffect() {
        openCount--;
        if (openCount !== 0) {
            return;
        }
        document.body.style.overflow = '';
        this.resetScrollbar();
        // this.resetAdjustments();
    },
    // 对话框引起的文档宽度超过浏览器窗口宽度,获取滚动条宽度this.scrollbarWidth
    checkScrollbar: function checkScrollbar() {
        var fullWindowWidth = window.innerWidth;
        if (!fullWindowWidth) {
            var documentElementRect = document.documentElement.getBoundingClientRect();
            fullWindowWidth = documentElementRect.right - Math.abs(documentElementRect.left);
        }
        this.bodyIsOverflowing = document.body.clientWidth < fullWindowWidth;
        if (this.bodyIsOverflowing) {
            this.scrollbarWidth = (0, _getScrollBarSize2["default"])();
        }
    },
    // 为滚动条增设document.body的右边距
    setScrollbar: function setScrollbar() {
        if (this.bodyIsOverflowing && this.scrollbarWidth !== undefined) {
            document.body.style.paddingRight = this.scrollbarWidth + 'px';
        }
    },
    // 清除document.body的右边距
    resetScrollbar: function resetScrollbar() {
        document.body.style.paddingRight = '';
    },

    // 执行this.props.onClose方法
    close: function close(e) {
        this.props.onClose(e);
    },

    // 由滚动条存在或对话框过高调整对话框的左右边距
    adjustDialog: function adjustDialog() {
        if (this.refs.wrap && this.scrollbarWidth !== undefined) {
            var modalIsOverflowing = this.refs.wrap.scrollHeight > document.documentElement.clientHeight;
            this.refs.wrap.style.paddingLeft = (!this.bodyIsOverflowing && modalIsOverflowing ? this.scrollbarWidth : '') + 'px';
            this.refs.wrap.style.paddingRight = (this.bodyIsOverflowing && !modalIsOverflowing ? this.scrollbarWidth : '') + 'px';
        }
    },
    resetAdjustments: function resetAdjustments() {
        if (this.refs.wrap) {
            this.refs.wrap.style.paddingLeft = this.refs.wrap.style.paddingLeft = '';
        }
    },

    render: function render() {
        var props = this.props;
        var prefixCls = props.prefixCls,
            maskClosable = props.maskClosable;

        var style = this.getWrapStyle();// 获取对话框包裹元素的样式

        // clear hide display
        // and only set display after async anim, not here for hide
        if (props.visible) {
            style.display = null;
        }
        return _react2["default"].createElement(
            "div", 
            null, 
            this.getMaskElement(), 
            _react2["default"].createElement(
                "div", 
                __assign({ 
                    tabIndex: -1, 
                    onKeyDown: this.onKeyDown, 
                    className: prefixCls + '-wrap ' + (props.wrapClassName || ''), 
                    ref: "wrap", 
                    onClick: maskClosable ? this.onMaskClick : undefined, 
                    role: "dialog", 
                    "aria-labelledby": props.title ? this.titleId : null, 
                    style: style 
                }, props.wrapProps), 
                this.getDialogElement()
            )
        );
    }
});
exports["default"] = Dialog;
module.exports = exports['default'];

 

LazyRenderBox.js

'use strict';

Object.defineProperty(exports, "__esModule", {
    value: true
});

var _react = require('react');
var _react2 = _interopRequireDefault(_react);

var _objectAssign = require('object-assign');
var _objectAssign2 = _interopRequireDefault(_objectAssign);

function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { "default": obj }; }

var __assign = undefined && undefined.__assign || Object.assign || function (t) {
    for (var s, i = 1, n = arguments.length; i < n; i++) {
        s = arguments[i];
        for (var p in s) {
            if (Object.prototype.hasOwnProperty.call(s, p)) t[p] = s[p];
        }
    }
    return t;
};

var LazyRenderBox = _react2["default"].createClass({
    displayName: 'LazyRenderBox',
    shouldComponentUpdate: function shouldComponentUpdate(nextProps) {
        return !!nextProps.hiddenClassName || !!nextProps.visible;
    },
    render: function render() {
        var className = this.props.className;
        if (!!this.props.hiddenClassName && !this.props.visible) {
            className += ' ' + this.props.hiddenClassName;
        }
        var props = (0, _objectAssign2["default"])({}, this.props);
        delete props.hiddenClassName;
        delete props.visible;
        props.className = className;
        return _react2["default"].createElement("div", __assign({}, props));
    }
});
exports["default"] = LazyRenderBox;
module.exports = exports['default'];

 

 

0
0
分享到:
评论

相关推荐

    dialog:React对话框

    var Dialog = require ( 'rc-dialog' ) ; ReactDOM . render ( &lt; Dialog xss=removed xss=removed&gt; &lt; p&gt; first dialog &lt; / p &gt; &lt; / Dialog &gt; ) , document . getElementById ( 't1' ) ) ; // use dialog ...

    single-document--dialog.zip_VC 文档 对话框_single

    资源文件(.rc)包含了这些定义,而头文件(.h)则生成了对应的C++类。 5. **对话框类(CDialog)**:MFC提供了CDialog基类来处理对话框,开发者可以继承这个类,添加自定义逻辑。 6. **嵌入对话框**:在SDI程序中...

    Modeless-dialog.rar_vc modeless dialog_visual c

    这个过程会生成一个.RC文件,其中包含了对话框资源的定义。 3. **实例化对话框**:在需要显示非模态对话框的地方,我们实例化之前定义的对话框类,并调用Create()函数来创建对话框窗口。Create()函数需要传入对话框...

    Python库 | dialog_bot_sdk-2.3.0rc3-py3-none-any.whl

    **Python库 | dialog_bot_sdk-2.3.0rc3-py3-none-any.whl** 在IT行业中,Python是一种广泛使用的编程语言,尤其在后端开发、数据分析、人工智能等领域。这个资源,"dialog_bot_sdk-2.3.0rc3-py3-none-any.whl",是...

    PyPI 官网下载 | dialog_bot_sdk-2.3.0rc3-py3-none-any.whl

    资源来自pypi官网。 资源全名:dialog_bot_sdk-2.3.0rc3-py3-none-any.whl

    改变对话框的背景色 设置各种背景色

    在Windows应用程序开发中,对话框(Dialog Box)是一种常见的用户界面元素,用于与用户进行交互。对话框可以包含各种控件,如按钮、文本框、复选框等,并且可以根据需求自定义其外观。本教程将详细介绍如何在...

    c++ Dialog 操作

    在资源脚本(.rc)文件中,这些控件都有唯一的ID。 2. **创建对话框类**:为了处理对话框中的事件,你需要创建一个继承自`CDialog`的类。这个类包含了对话框的逻辑,并且可以通过`OnInitDialog()`函数初始化控件的...

    VC6 工程支持多个 rc 文件

    #include "dialog1.rc" #include "dialog2.rc" ``` 这使得主rc文件能够访问并编译其他rc文件中的资源。 4. 更新项目设置:打开“工程”-&gt;“设置”菜单,转到“资源”选项卡。在“资源编译器”部分,添加额外rc文件...

    在Dialog中使用Menu和Toolbar

    首先,你需要创建一个资源文件(如.rc),并在其中定义菜单项。然后,通过`LoadMenu`函数加载资源中的菜单,并使用`SetMenu`将菜单设置到Dialog上。 2. **Toolbar介绍** Toolbar是包含按钮的水平条,用于快速执行...

    Dialog设计,关于shell 脚本的处理

    2. **--create-rc file**: 使用这个选项可以生成一个默认配置文件,这对于定制 `Dialog` 的行为很有帮助。 3. **--title title** 和 **--backtitle backtitle**: 分别用于设置对话框的主标题和背景标题,提供更好的...

    不用向导建立WIN32 Dialog工程解析

    在Windows编程领域,Win32 API(应用程序接口)是开发者构建桌面应用的基础,而Dialog Box则是其中一种重要的交互元素。本篇文章将详细解析如何在不使用Visual Studio等IDE的向导辅助下,手动创建一个Win32 Dialog...

    MFC多Dialog之间通信

    通过对话框资源(.rc文件)定义了对话框的外观,而.CPP和.H文件则定义了对话框的类和成员函数。 1. **消息映射**:在CDialog派生类的头文件中,定义消息映射(ON_BN_CLICKED, ON_EN_CHANGE等)来响应用户在对话框上...

    基于MFC创立的dialog

    1. **创建资源文件**:首先,你需要在Visual Studio中创建一个新的资源文件(.rc文件),并在其中添加一个对话框模板。这个模板定义了对话框的大小、控件布局以及它们的属性。 2. **设计对话框**:使用Visual ...

    RC 资源文件字串抽取器

    1. 读取RC文件:程序会逐行读取文件内容,识别关键的资源类型关键字,如`STRINGTABLE`、`DIALOG`和`MENU`。 2. 解析资源结构:识别到这些关键字后,程序会解析其后的结构,包括括号内的参数,如控件ID、坐标、尺寸等...

    MFCDialog.zip_MFCDialog

    在"MFCDialog"项目中,可能会有一个名为"MFCDialog.rc"的文件,里面定义了对话框的外观和控件。 **对话框的生命周期:** - **创建**:通常,对话框通过DoModal()函数创建并显示。这个函数会阻塞调用线程,直到...

    VC 超多Dialog对话框操作源码大全.rar

    VC 超多Dialog对话框操作源码实例大全,这些例子是:创建模式对话框  创建消息对话框  改变对话框中控件的颜色  关闭非模式对话框  扩展对话框的大小  实现对话框在桌面工作区的停靠  使对话框居中显示 ...

    VS2010 VC++创建.rc资源文件(一)

    在Visual Studio 2010 (VS2010) 中使用VC++进行Windows应用程序开发时,资源文件(.rc)扮演着至关重要的角色。资源文件是包含各种非代码元素,如窗口、菜单、图标、对话框等的文本文件,它们为程序提供了用户界面。...

    VC++源代码 Dialog详解,各种对话框详解

    在VC++编程环境中,Dialog是Windows应用程序中一个重要的交互元素,它用于显示用户可以与之交互的窗口,如输入信息、选择选项或执行特定操作。本资料主要详细讲解了Dialog的使用及其各种类型,旨在帮助开发者更好地...

    MFC的Dialog贴图

    在MFC(Microsoft Foundation Classes)框架中,对话框(Dialog)是用户界面的重要组成部分,用于显示和收集用户信息。在一些应用中,我们可能需要在对话框上添加自定义的背景图片,以提升用户界面的视觉效果。"MFC...

    Dialog.rar

    对话框在Windows程序中通常作为资源存在,这些资源在.rc文件中定义。VS2015的资源编辑器允许开发者可视化地编辑这些资源,包括对话框的大小、位置以及上面的控件。 6. **消息映射和处理** 在MFC中,消息映射机制...

Global site tag (gtag.js) - Google Analytics