`

平台新的双向选择组件

    博客分类:
  • js
阅读更多
1.index.jsp
<%@ page contentType="text/html; charset=GBK" %>

<!-- 定义组件按钮样式 -->
<style>
.button {
width: 50px;
height: 22px;
font-size: 10px;
color: #000000;
border: #6B698C 1px solid;
cursor: hand;
vertical-align: middle;
}
</style>

<script type="text/javascript" src="scripts\selectlist.js"></script>

<script language="javascript">

function checkMenu(mid){
  //判断是否选中,组件需要调用
  var menus = [23,25];
  for(var i = 0; i < menus.length; i++){
    if(mid == menus[i]){
      return true;
    }
  }
  return false;
}

//准备组件数据
//数据项分别是:选中时的值(内码),显示名,是否选中,排序码(外码),级次
var items =
[
  [ "21", "测试管理", checkMenu(21), "200", 1],
  [ "22", "测试单表", checkMenu(22), "200001", 2],
  [ "23", "系统管理", checkMenu(23), "800", 1],
  [ "24", "组织管理", checkMenu(24), "800800", 2],
  [ "25", "基本机构信息", checkMenu(25), "800800700", 3],
  [ "26", "组织机构维护", checkMenu(26), "800800800", 3],
  [ "27", "职务管理", checkMenu(27), "800800900", 3],
  [ "28", "用户管理", checkMenu(28), "800900", 2],
  [ "29", "增加用户信息", checkMenu(28), "800900700", 3],
  [ "30", "用户信息维护", checkMenu(30), "800900800", 3]
];

//实例化对象,参数分别是:要显示的列表数据,选择框的显示大小,是否以树状形式显示,是否显示层次码
var gbp_sl = new GBPSelectList("org", items, 10, 1, 1, "button");
var gbp_sl_my = new GBPSelectList("myorg", items, 10, 1, 0, "button");
var gbp_sl_list = new GBPSelectList("listorg", items, 10, 0, 0, "button");

</script>

<html>
  <head>
    <title>报告编制</title>
  </head>
  <body>
    <form name="frmMain">
      <table width="600px">
        <tr bgcolor=#AAAA00>
          <td>
      <script>gbp_sl.make();</script>
          </td>
        </tr>
        <tr bgcolor=#AAAA00>
          <td>
      <script>gbp_sl_my.make();</script>
          </td>
        </tr>
        <tr bgcolor=#AAAA00>
          <td>
      <script>gbp_sl_list.make();</script>
          </td>
        </tr>
      </table>
    </form>
  </body>
</html>
2.select.js
/*
-------------------------------------------------------------------------------
文件名称:select.js
说    明:JavaScript脚本,用于网页中涉及到选择时的控制,
版    本:1.0
修改纪录:
---------------------------------------------------------------------------
时间 修改人 说明
2004-8-11 zxl 创建
-------------------------------------------------------------------------------
*/

/*
用途:菜单配置页面中一级菜单被点击
输入: form对象
1.选中和单项选择一样,变颜色
2.没选中时则下级都不能选中
add by zbk
*/
 
function CA(CB){
if(CB.checked){

}else{
var frm=document.forms[0];
  for (var i=0;i<frm.elements.length;i++)
  {
    var e=frm.elements[i];
    if (  (e.type=='checkbox')&&
    (CB.id.indexOf(e.id.substring(0,3))==0)&&e.disabled==false)
    {
      e.checked=false;
     
    }
  }
  }

}
/*
用途:菜单配置页面中底级菜单项被点击
输入: form对象
1.对象选中则上级被选中,变颜色
2.对象没被选中则下级取消,变颜色
add by zbk
*/

function CCA(CB){//菜单配置页面中子菜单项被点击
  if (CB==null) return; 
  var frm=document.forms[0];
 
  if (CB.checked){
    for (var i=0;i<frm.elements.length;i++)
    {
      var e=frm.elements[i];
      if((e.type=='checkbox')&&(e.name=='menucheckbox')&&(CB.id.indexOf(e.id)==0)&&e.disabled==false)
      {
        e.checked=true;
       
      }
    }
  }
  else{
 
   for (var i=0;i<frm.elements.length;i++)
  {
    var e=frm.elements[i];
    if ( (e.type=='checkbox')&&
    (e.id.indexOf(CB.id)==0)&&e.disabled==false)
    {
      e.checked=false;

    }
  }
 
  }
}

/*
用途:如果只有一个选择框得到他的值
输入: chekname选择框对象名称
add by zbk
*/

function getselectedValue(chekname){
if (chekname==null) return ''; 
  var length =0;
    var i=0;
    var count =0;
var frm=document.forms[0];
    eles = frm.elements;
for (var i=0;i<frm.elements.length;i++)
    {
obj= eles.item(i);
      type = obj.attributes.item("type").nodeValue;
      if(type == "checkbox" && obj.disabled==false &&obj.id!="selectAll" && obj.id==chekname && obj.checked){
  return obj.value;
  }
    }

}

// 返回true表示选择,返回false表示没有选择
function selectedRadio(form){
    var length =0;
    var i=0;   
    eles = form.elements;
   
    while( i<eles.length){
      obj= eles.item(i);
      type = obj.attributes.item("type").nodeValue;
      if(type == "radio"){
        if(obj.checked){
       
          return true;
        }
      }
      i++;
    }
    return false;
  }    
3.selectlist.js
/**
组件名称:
  左右双向选择组件。

主要功能:
  1> 提供两种风格-普通列表和树状列表。
  2> 目前提供三个扩展函数
    getSelectedValues:  获得选中的值,返回的是个包含节点编号的数组
    getRows:            查看所有记录的详细信息,返回的是个包含节点对象的二维数组
    getSelectedRows:    查看选中记录的详细信息,返回的是个包含节点对象的二维数组

备注说明:
  树状风格时,选择上级会自动级联处理下级;选择下级则上级默认半选中;如果所有下级都选中,则上级选中。
  一个页面只能使用一个组件(需要修改)

修改记录
  2007-06-22
  杨文彦
  整理代码,添加注释,增加按钮样式
*/

/*==================== 组件对象代码 ====================*/
/*
1、构造方法
2、数据检查过滤方法
3、组件生成方法
4、选中/取消方法
5、全部选中/取消方法
6、页面数据显示方法*/

//构造函数
function GBPSelectList(nameSpace, items, size, isTreeStyle, isShowCode, ctrlStyle) {
    this.multiple            = true;                    //是否允许多选
    this.listSize            = size ? size : 20;        //选择框的长度
    this.isTreeStyle         = Boolean(isTreeStyle);    //是否用树状形式显示数据
    this.isShowCode          = Boolean(isShowCode);     //如果是树状形式,是否显示层次码
    this.ctrlStyle           = ctrlStyle;               //按钮样式

    this.needOrder           = true;                   //是否需要排序。(只在普通形式下起作用,树状时必须排序)

    this.itemList            = items;                   //存放所有选择元素的数组

    this.leftSelect          = null;                    //左边的选择框控件
    this.rightSelect         = null;                    //右边的选择框控件
    this.leftDiv             = null;                    //左边放置列表框的层
    this.rightDiv            = null;                    //右边放置列表框的层
    this.leftDiv_innerHTML   = "";                      //左侧选择框层的内部元素,
    this.rightDiv_innerHTML  = "";                      //右侧选择框层的内部元素
    this.leftLabel           = null;                    //左边的文字标签控件
    this.rightLabel          = null;                    //右边的文字标签控件

    this.namespace           = nameSpace;               //控件名称的前缀
    this.leftDivName         = "divAll";                //左边放置列表框的层的名称
    this.rightDivName        = "divChecked";            //右边放置列表框的层的名称
    this.leftSelectName      = "sltAll";                //左边列表框的名称
    this.rightSelectName     = "sltChecked";            //右边列表框的名称
    this.selectButton        = "btnSelect";             //选中按钮的名称
    this.selectAllButton     = "btnSelectAll";          //选中所有按钮的名称
    this.cancelButton        = "btnCancel";             //取消选择按钮的名称
    this.cancelAllButton     = "btnCancelAll";          //取消所有按钮的名称

    this.o_children          = [];                      //一级节点列表
    this.root                = this;                    //根级节点-组件对象
    this.nodes               = {};                      //存放所有节点
    var o_this               = this;                    //组件对象引用

    this.buildNode = function(){                        //处理组件显示数据,建立节点列表对象
        if(this.isTreeStyle){
            //构造树形显示风格
            _build(o_this, 0);
        }else{
            //构造列表显示风格
            for(var i = 0; i < this.itemList.length; i++){
                new GBP_SL_Item(o_this, this.itemList[i]);
            }
        }
    };

    //在子类里迭代,是为了效率考虑。
    function _build(father, level, start){
        if(!start) start = 0;
        for(var i = start; i < o_this.itemList.length; i++){
            curLevel = o_this.itemList[i][4];
            //是否属于本级节点
            if(curLevel == (level + 1)){
                var obj = new GBP_SL_Item(father, o_this.itemList[i]);
                //继续下层递归
                _build(obj, curLevel, i + 1);
            }else if(curLevel <= level){
                //结束本层递归
                break;
            }
        }
    }
}

//对数据进行检查、过滤、排序处理
GBPSelectList.prototype.prepareData = function() {
    var list = new Array();
    var arrays = this.itemList;
    var _pre;

    //按显示名排序
    function _sortByName(a,b){
        if(a[1] == b[1]) return 0
        return (a[1] > b[1]) ? 1 : -1;
    }

     //按层次码排序
    function _sortByTreeCode(a,b){
        if(a[3] == b[3]) return 0
        return (a[3] > b[3]) ? 1 : -1;
    }

    //检查数据是否完整,过滤不完整数据
    for(var n = 0; n < arrays.length; n++){
        var len = list.length;
        //列表节点至少要求有两项数据
        //树形节点至少要求有五项数据
        if((!this.isTreeStyle && arrays[n].length >= 2) || (this.isTreeStyle && arrays[n].length >= 5)){
            list[len] = arrays[n];
        }
    }

    //排序
    if(this.isTreeStyle){
        list.sort(_sortByTreeCode);
    }else if(this.needOrder){
        list.sort(_sortByName);
    }

    //使用已经排序的节点列表替换原来未排序的节点列表
    this.itemList = list;
}

//构造组件,并显示在页面上
GBPSelectList.prototype.make = function() {
    //检查、过滤、排序数据
    this.prepareData();

    var txt , obj;

    txt = "<table width='100%' height='100' border='0' align='center' cellpadding='0' cellspacing='0'>"
             + " <tr align='center'>"
             + "   <td valign='top' width='30%'><div align='left' style='FONT-SIZE: 9pt;'>可用列表</div>"
             + "     <div id='" + this.namespace + this.leftDivName + "'></div>"
             + "   </td>"
             + "   <td valign='middle' width='8%' >"
             + "     <input type='button' name='" + this.namespace + this.selectButton + "' class='" + this.ctrlStyle + "' value='  >  '><br><br>"
             + "     <input type='button' name='" + this.namespace + this.selectAllButton + "' class='" + this.ctrlStyle + "' value='  >> '><br><br>"
             + "     <input type='button' name='" + this.namespace + this.cancelButton + "' class='" + this.ctrlStyle + "' value='  <  '><br><br>"
             + "     <input type='button' name='" + this.namespace + this.cancelAllButton + "' class='" + this.ctrlStyle + "' value=' <<  '>"
             + "   </td>"
             + "   <td valign='top' width='30%'><div align='left' style='FONT-SIZE: 9pt;'>选择列表</div>"
             + "     <div id='" + this.namespace + this.rightDivName + "' align='left'></div>"
             + "   </td>"
             + " </tr>"
             + "</table>" ;

    ///生成对象
    document.write(txt);
   
    var oThis = this;
   
    //关联控制对象
    this.leftDiv = document.getElementById(this.namespace + this.leftDivName);
    this.rightDiv = document.getElementById(this.namespace + this.rightDivName);

    //关联按钮单击事件
    obj = document.getElementById(this.namespace + this.selectButton);
    obj.onclick = function() { oThis.doSelect(1) };

    obj = document.getElementById(this.namespace + this.selectAllButton);
    obj.onclick = function() { oThis.doAll(1) };

    obj = document.getElementById(this.namespace + this.cancelButton);
    obj.onclick = function() { oThis.doSelect(-1) };

    obj = document.getElementById(this.namespace + this.cancelAllButton);
    obj.onclick = function() { oThis.doAll(-1) };

    //生成节点对象列表
    this.buildNode();

    //刷新页面显示
    this.update();
};

//选中/取消选中
GBPSelectList.prototype.doSelect = function(isSelect) {
    var values = new Array();

    //处理哪个对象
    var obj = (isSelect == 1) ? this.leftSelect : this.rightSelect;

    //判断是否多选模式
    if(this.multiple){
        for(var i = 0; i < obj.options.length; i++){
            if(obj.options[i].selected){
                values[values.length] = obj.options[i].value;
            }
        }
    }else{
        if(obj.selectedIndex > -1){
            values[0] = obj[obj.selectedIndex].value;
        }
    }

    if( values.length == 0) return;

    //处理下级节点
    for(var n = 0; n < values.length; n++){
        this.nodes[values[n]].doSelect(isSelect);
    }

    //刷新页面显示
    this.update()
};

//全部选中/取消
GBPSelectList.prototype.doAll = function(isSelect) {
    for(var i = 0; i < this.o_children.length; i++){
        this.o_children[i].doSelect(isSelect);
    }

    this.update()
};

//更新select元素的界面显示
GBPSelectList.prototype.update = function(){
    //用innerHTML来设置select元素,可以加快速度,减少屏幕闪烁
    //给层元素赋初值
    this.rightDiv_innerHTML = "<select name='" + this.namespace + this.rightSelectName + "' " + (this.multiple ? "multiple" : "") + " size='" + this.listSize + "' style='width:100%'>";
    this.leftDiv_innerHTML = "<select name='" + this.namespace + this.leftSelectName + "' " + (this.multiple ? "multiple" : "") + " size='" + this.listSize + "' style='width:100%'>";

    //处理子节点状态,根据子节点的状态设置父节点状态
    for(var i = 0; i < this.o_children.length; i++){
        this.o_children[i].doStatus();
    }
    //生成页面显示
    for(var i = 0; i < this.o_children.length; i++){
        this.o_children[i].update();
    }

    //直接用innerHTML,速度最快
    this.leftDiv.innerHTML = this.leftDiv_innerHTML + "</select>";
    this.rightDiv.innerHTML = this.rightDiv_innerHTML + "</select>";

    //重新获取两侧select元素,并为其绑定双击事件
    var oThis = this;

    this.leftSelect = document.getElementById(this.namespace + this.leftSelectName);
    this.leftSelect.ondblclick = function() { oThis.doSelect(1) };

    this.rightSelect  = document.getElementById(this.namespace + this.rightSelectName);
    this.rightSelect .ondblclick = function() { oThis.doSelect(-1) };
};

/*==================== 节点对象代码 ====================*/
/*
1、构造方法
2、节点选择/取消方法
3、节点状态处理方法
4、节点页面显示实现方法*/

//节点对象
function GBP_SL_Item(o_parent, o_item){
    this.father = o_parent;                                //上级节点引用
    this.root = o_parent.root;                             //根级节点引用
    this.o_children = [];                                  //下级节点数组
    this.info = o_item;                                    //节点对象
    this.status = o_item[2] ?  1 : -1 ;                    //节点是否默认选中标志
    this.level = this.root.isTreeStyle ? o_item[4] : 1;    //节点级次
    this.value = o_item[0];                                //节点编号
    this.showText = o_item[1];                             //节点显示名称

    //根据定义处理显示名称格式
    if(this.root.isShowCode)
        // 是否显示层级编码
        this.showText = this.showText + " - " + o_item[3];

    if(this.level > 0)
        //处理显示缩进
        this.showText = "__________________".substr(0, (this.level - 1)).replace(/_/g, "&nbsp;&nbsp;") + this.showText;

    //加入父级节点的下级节点列表
    o_parent.o_children[o_parent.o_children.length] = this;

    //加入所有节点列表
    this.root.nodes[this.value] = this;
}

//节点的选择函数
GBP_SL_Item.prototype.doSelect = function(isSelected){
    this.status = isSelected;

    //选择上级会自动选择其所有下级
    for(var i = 0; i < this.o_children.length; i++){
        this.o_children[i].doSelect(isSelected);
    }
};

//递归处理指定节点及其下级节点状态
GBP_SL_Item.prototype.doStatus = function(){
    //没有下级节点
    if(this.o_children.length == 0) return this.status;

    var selectedCount = 0, partSelectCount = 0, unselectedCount = 0;
    var returnStatus = null;

    for(var i = 0; i < this.o_children.length; i++){
        //递归处理下级节点状态
        returnStatus = this.o_children[i].doStatus();

        if(returnStatus == 1) selectedCount += 1;
        if(returnStatus == 0) partSelectCount += 1;
        if(returnStatus == -1) unselectedCount += 1;
    }

    if(selectedCount > 0 && unselectedCount == 0 && partSelectCount == 0){
        //下级全部选中
        this.status = 1;
    }else if(unselectedCount > 0 && selectedCount == 0 && partSelectCount == 0){
        //下级一个都没选中
        this.status = -1;
    }else{
        //下级部分选中
        this.status = 0;
    }

    return this.status;
};

//更新界面显示
GBP_SL_Item.prototype.update = function(){
    //status:1表示选中;0表示下级部分选中;-1表示未选
    if(this.status == -1 || this.status == 0){
        this.root.leftDiv_innerHTML += "<option value='" + this.value + "'>" + this.showText + "</option>\n";
    }

    if(this.status == 1 || this.status == 0){
        this.root.rightDiv_innerHTML += "<option value='" + this.value + "'>" + this.showText + "</option>\n";
    }

    //递归处理下级节点
    for(var i = 0; i < this.o_children.length; i++){
        this.o_children[i].update();
    }
};

/*==================== 组件扩展方法 ====================*/

//获得选中的值,返回的是个包含节点编号的数组
GBPSelectList.prototype.getSelectedValues = function() {
    var values = new Array();
    for(var n = 0; n < this.rightSelect.options.length; n++){
        values[n] = this.rightSelect.options[n].value;
    }
    return values;
};

//查看右侧记录的详细信息,返回的是个包含节点对象的二维数组
GBPSelectList.prototype.getRows = function() {
    var values = new Array();
    for(var n = 0; n < this.rightSelect.options.length; n++){
        values[values.length] = this.root.nodes[this.rightSelect.options[n].value].info;
    }
    return values;
};

//查看选中记录的详细信息,返回的是个包含节点对象的二维数组
GBPSelectList.prototype.getSelectedRows = function() {
    var values = new Array();
    for(var n = 0; n < this.rightSelect.options.length; n++){
        if(this.rightSelect[n].selected)
            values[values.length] = this.root.nodes[this.rightSelect.options[n].value].info;
    }
    return values;
};
分享到:
评论

相关推荐

    一个可双向滑块选择器的微信小程序组件double-sided-slider-master.zip

    "double-sided-slider-master.zip" 提供了一个可双向滑块选择器的组件,适用于那些需要用户同时选择两个范围或者数值的情况,比如日期区间、价格范围等。下面将详细解释这个组件的核心知识点: 1. **组件结构**: ...

    手动封装silder双向滑块组件

    本话题将详述如何手动封装一个双向滑块组件,解决小程序内置silder组件不能双向选择的问题。 首先,我们要理解小程序的基本架构。小程序由WXML(WeiXin Markup Language)和WXSS(WeiXin Style Sheet)负责视图层的...

    vue.js版双向滑动区间选择器

    Vue.js版双向滑动区间选择器是一款专为移动端设计的前端组件,旨在解决在不同设备和浏览器上输入范围时可能出现的兼容性问题,提供优秀的用户体验。这个组件基于流行的JavaScript前端框架Vue.js,使得开发者能够轻松...

    jquery双向选择器代码

    "jquery双向选择器代码"这个主题涉及的是jQuery中一种特殊的选择器机制,它允许我们从DOM(文档对象模型)中选取元素,同时还能根据某个元素选取其关联的元素,实现数据的双向绑定或者元素间的交互效果。这种功能在...

    基于bootstrap的双向选择器

    双向选择器在实际开发中的应用广泛,无论是后台管理系统、电子商务平台还是各种配置工具,都可以看到它的身影。通过合理利用这个组件,开发者可以提升产品的交互性和易用性,为用户提供更流畅的操作体验。

    vue时间选择组件.zip

    2. **数据绑定**:Vue的双向数据绑定使得组件的状态可以实时反映在视图上,反之亦然,用户操作可以即时更新组件状态。 3. **计算属性与方法**:利用Vue的计算属性来处理时间间隔计算和格式化,以及方法来处理用户...

    vuejs组件-选择人员

    3. **数据绑定**:Vue.js的核心特性之一就是双向数据绑定,通过`data`属性定义组件的初始数据,并使用`{{ }}`插值表达式或`v-bind`指令将数据绑定到视图。 4. **组件通信**:在组件之间传递数据,可以使用props(父...

    小程序双向滑动组件双向slider滑动微信小程序组件slider组件

    在标题和描述中提到的“小程序双向滑动组件”即指的是这个组件,它允许用户通过左右滑动来选择不同选项或者浏览内容。下面将详细探讨`slider`组件的核心特性、用法以及如何在实际项目中应用。 一、`slider`组件的...

    vue国家区号选择组件

    1. 数据绑定:组件的内部数据(如国家列表、选中状态等)可以与视图双向绑定,确保UI实时反映数据的变化。 2. 计算属性和侦听器:可能使用计算属性来处理国家列表的排序或过滤,同时通过侦听器监听用户的选择事件,...

    Vue数据双向绑定以及组件化简介

    ### Vue.js 数据双向绑定原理与组件化开发详解 #### 一、数据双向绑定概念与原理 数据双向绑定是Vue.js框架的核心特性之一,它使得数据模型(Model)和视图(View)之间的同步变得非常简单且高效。在传统的前端...

    jquery实现 左右 双向选择器 挺好用

    在给定的资源中,“jquery实现 左右 双向选择器 挺好用”是一个利用jQuery创建的交互式用户界面组件,用于实现双向选择功能。这种组件常见于需要用户在两个列表之间转移选项的场景,例如在“已选”和“可选”之间...

    js实现左右双向选择器

    在JavaScript编程中,"js实现左右双向选择器"是一个常见的交互设计元素,它允许用户在两个独立的列表之间进行选择和转移项目。这种选择器通常用于处理数据的筛选、分类或者关联,例如在音乐播放器中选择歌曲列表或在...

    bootstrap双向选择

    Bootstrap 双向选择插件是一种基于流行的前端框架 Bootstrap 开发的组件,主要用于创建美观、功能丰富的选择列表。这种插件通常用于实现用户在两个列表之间移动条目的功能,例如在“已选”和“未选”之间转移项目,...

    微信小程序双向滑动slider(区间选择器).zip

    微信小程序的双向滑动slider,也称为区间选择器,是一种常见的用户交互组件,常用于价格范围、评分等需要用户在两个数值之间进行选择的场景。本文将深入探讨如何在微信小程序中实现这样的功能。 首先,我们需要了解...

    vue20的移动端日期选择组件

    在Vue.js 2.0的移动端应用开发中,日期选择组件是不可或缺的一部分,它使得用户能够方便地输入或选择日期,提升用户体验。Vue Calendar是一款专为Vue 2.0设计的日期选择组件,适用于各种移动设备。下面我们将深入...

    Vue国际电话区号选择组件

    Vue国际电话区号选择组件是一种常用的前端开发工具,主要用于在Web应用中实现用户输入电话号码时选择国家或地区代码的功能。Vue.js是一个轻量级、高性能的JavaScript框架,以其组件化开发模式和易于上手的特点深受...

    jquery双向选择器两侧

    标题 "jquery双向选择器两侧" 暗示我们讨论的是一个基于jQuery的特定插件或功能,它允许在用户界面的两侧实现双向交互的选择器。这个选择器可能常见于导航菜单、过滤系统或者数据同步的场景,使得用户可以从两个独立...

    jquery制作双向选择移动器特效

    标题 "jquery制作双向选择移动器特效" 描述的是使用JavaScript库jQuery实现的一种交互式UI组件。这个组件通常被称为“选择器”或者“选择框”,它允许用户在两个列表之间移动项目,例如在“可用”和“已选”之间进行...

    移动端日期选择vue组件

    在Vue.js中,你可以通过`v-model`双向绑定日期选择器的值到表单数据,这样在提交表单时,选定的日期会自动包含在表单数据中。还可以结合`v-bind:disabled`或`v-if`指令实现表单验证和控制日期选择器的显示。 总结,...

    双向选择器

    标题中的“双向选择器”通常指的是在用户界面设计中一种特殊的控件,允许用户在两个独立的列表之间转移选项,这种交互方式常用于数据筛选、设置配置或信息分类等场景。这种选择器的一个典型实现是基于Bootstrap框架...

Global site tag (gtag.js) - Google Analytics